Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
In this article, we will create a typing game in React. This typing game will usually be like other typing games where we can get accuracy and words per minute as a result. Here we will have a timer, then a paragraph, and lastly an input field for user input. The timer and paragraph will be started as soon start button gets clicked, the timer will be 60 seconds, and then the user will be able to write in the input field. As soon the timer runs out then accuracy and wpm will appear as a result.
So basically this is going to be a beginner-friendly project, so let’s make this project step-by-step.
Now let’s create a basic UI for our project, for that we will move to App.js. Here, we have imported useState, useEffect, and useRef hooks which are going to be helpful throughout the project. Then we also imported the random-words package, which is going to be used to generate random words. Now we have declared constants for a number of words of 400, and seconds to 60.
Then we created a state for words, and then we created a useEffect hook where we have set the words state where we will get words from generateWords()
function. In this function, we’re just returning an array of words using this line of code return new Array(NUMB_OF_WORDS).fill(null).map(() => randomWords())
.
Now in the return() statement, we have added a <h2> tag for the timer, then we have added an input field, and a button and after that we have words.map((word, i) => (<span key={i}>) to add spacing between the letters.
import {useState, useEffect, useRef} from 'react'
import randomWords from 'random-words'
const NUMB_OF_WORDS = 400
const SECONDS = 60
function App() {
const [words, setWords] = useState([])
useEffect(() => {
setWords(generateWords())
}, [])
function generateWords() {
return new Array(NUMB_OF_WORDS).fill(null).map(() => randomWords())
}
return (
<div className="App">
<div className="section">
<div className="is-size-1 has-text-centered has-text-primary">
<h2>{SECONDS}</h2>
</div>
</div>
<div className="control is-expanded section">
<input className="input" />
</div>
<div className="section">
<button className="button is-info is-fullwidth" >
Start
</button>
</div>
<div className="section" >
<div className="card">
<div className="card-content">
<div className="content">
{words.map((word, i) => (
<span key={i}>
<span>
{word }
</span>
<span> </span>
</span>
))}
</div>
</div>
</div>
</div>
</div>
);
}
export default App;
After that, we will set up a countdown, For that we have added a click event on the button with the start() function call. In this function, we have to update the countdown state, here if the prevCountdown is equal to 0 then we have to clear the interval and just return back seconds, we will decrease the prevCountdown by 1. Simply prevCountdown consists second’s value which is 60. We are returning the decreased value and updating the setCountdown with its value.
function start() {
let interval = setInterval(() => {
setCountDown((prevCountdown) => {
if (prevCountdown === 0) {
clearInterval(interval)
return SECONDS
} else {
return prevCountdown - 1
}
} )
} , 1000 )
}
Now we have to match words, and for that, we have updated our input field with some basic things like we have added a function call on the keyDown event, and we have updated the currInput value with user input. Then we have to target each and every word using {word.split(“”).map((char, idx) => (<span key={idx}>{char}</span>)) } this line of code, So this is all we have to update in the return statement, now we need to define the mentioned functions for matching words.
Ok, so handleKeyDown() function, we have added a keycode for the space button which is 32. here we have called a check function, and we have also added another state currInput, and currWordIndex to monitor input and each word index. We have updated the input with a null value and the word index with an increased value.
In checkMatch() function, we simply, assign a constant for the word which we simply compare with the indexed value of the paragraph. Here we have broke the paragraph into the word array with respect to space, so here words[currWordIndex] will return only a word, and we are increasing the value of the index in the handleKeyDown() function to get the next word.
<div className="control is-expanded section">
<input type="text" className="input" onKeyDown={handleKeyDown} value={currInput} onChange={(e) => setCurrInput(e.target.value)} />
</div>
<div className="section" >
<div className="card">
<div className="card-content">
<div className="content">
{words.map((word, i) => (
<span key={i}>
<span>
{word.split("").map((char, idx) => (
<span key={idx}>{char}</span>
)) }
</span>
<span> </span>
</span>
))}
</div>
</div>
</div>
</div>
function handleKeyDown({keyCode, key}) {
// space bar
if (keyCode === 32) {
checkMatch()
console.log(currInput)
setCurrInput("")
setCurrWordIndex(currWordIndex + 1)
// backspace
}
}
function checkMatch() {
const wordToCompare = words[currWordIndex]
const doesItMatch = wordToCompare === currInput.trim()
console.log(doesItMatch);
}
Now for the result section, we have added some divs and paragraphs with the initial value correct which is actually our new state, also we have an incorrect state. We have added word per minute as a correct value and to map accuracy we have used one of the math function line {Math.round((correct / (correct + incorrect)) * 100)}%. Then we just update the value of correct and incorrect in the checkMatch() function.
<div className="section">
<div className="columns">
<div className="column has-text-centered">
<p className="is-size-5">Words per minute:</p>
<p className="has-text-primary is-size-1">
{correct}
</p>
</div>
<div className="column has-text-centered">
<p className="is-size-5">Accuracy:</p>
<p className="has-text-info is-size-1">
{Math.round((correct / (correct + incorrect)) * 100)}%
</p>
</div>
</div>
</div>
function checkMatch() {
const wordToCompare = words[currWordIndex]
const doesItMatch = wordToCompare === currInput.trim()
if (doesItMatch) {
setCorrect(correct + 1)
} else {
setIncorrect(incorrect + 1)
}
}
Now to loop the game, we have to define another state named status. Firstly, we have defined useEffect, where if the status is started then the input will get focus() method initialized. Then in start() function, we have some default values for states, and in the word state, we have called the generateWords() function to get the function as soon start() will run. Lastly, we have added conditions to check the status state’s value, and according to them, we will show sections if the status is equal to started then only the writing section and paragraph will be visible and if the status is equal to finish then the result section will appear.
import {useState, useEffect, useRef} from 'react'
import randomWords from 'random-words'
const NUMB_OF_WORDS = 200
const SECONDS = 5
function App() {
const [words, setWords] = useState([])
const [countDown, setCountDown] = useState(SECONDS)
const [currInput, setCurrInput] = useState("")
const [currWordIndex, setCurrWordIndex] = useState(0)
const [correct, setCorrect] = useState(0)
const [incorrect, setIncorrect] = useState(0)
const [status, setStatus] = useState("waiting")
const textInput = useRef(null)
useEffect(() => {
setWords(generateWords())
}, [])
useEffect(() => {
if (status === 'started') {
textInput.current.focus()
}
}, [status])
function generateWords() {
return new Array(NUMB_OF_WORDS).fill(null).map(() => randomWords())
}
function start() {
if (status === 'finished') {
setWords(generateWords())
setCurrWordIndex(0)
setCorrect(0)
setIncorrect(0)
}
if (status !== 'started') {
setStatus('started')
let interval = setInterval(() => {
setCountDown((prevCountdown) => {
if (prevCountdown === 0) {
clearInterval(interval)
setStatus('finished')
setCurrInput("")
return SECONDS
} else {
return prevCountdown - 1
}
} )
} , 1000 )
}
}
function handleKeyDown({keyCode, key}) {
// space bar
if (keyCode === 32) {
checkMatch()
setCurrInput("")
setCurrWordIndex(currWordIndex + 1)
// backspace
}
}
function checkMatch() {
const wordToCompare = words[currWordIndex]
const doesItMatch = wordToCompare === currInput.trim()
if (doesItMatch) {
setCorrect(correct + 1)
} else {
setIncorrect(incorrect + 1)
}
}
return (
<div className="App">
<div className="section">
<div className="is-size-1 has-text-centered has-text-primary">
<h2>{countDown}</h2>
</div>
</div>
<div className="control is-expanded section">
<input ref={textInput} disabled={status !== "started"} type="text" className="input" onKeyDown={handleKeyDown} value={currInput} onChange={(e) => setCurrInput(e.target.value)} />
</div>
<div className="section">
<button className="button is-info is-fullwidth" onClick={start}>
Start
</button>
</div>
{status === 'started' && (
<div className="section" >
<div className="card">
<div className="card-content">
<div className="content">
{words.map((word, i) => (
<span key={i}>
<span>
{word.split("").map((char, idx) => (
<span key={idx}>{char}</span>
)) }
</span>
<span> </span>
</span>
))}
</div>
</div>
</div>
</div>
)}
{status === 'finished' && (
<div className="section">
<div className="columns">
<div className="column has-text-centered">
<p className="is-size-5">Words per minute:</p>
<p className="has-text-primary is-size-1">
{correct}
</p>
</div>
<div className="column has-text-centered">
<p className="is-size-5">Accuracy:</p>
{correct !== 0 ? (
<p className="has-text-info is-size-1">
{Math.round((correct / (correct + incorrect)) * 100)}%
</p>
) : (
<p className="has-text-info is-size-1">0%</p>
)}
</div>
</div>
</div>
)}
</div>
);
}
export default App;
Now in the return statement, we have added a getCharClass in the word section, in this function, we are checking some conditions to run like word index, character index, and status value. If the character is matched then we will add a success color value and if it doesn’t match then we will add a danger value color which is red color. So if the character is mismatched then the character’s background will change to red color and if it matches then the color will be green color.
{words.map((word, i) => (
<span key={i}>
<span>
{word.split("").map((char, idx) => (
<span className={getCharClass(i, idx, char)} key={idx}>{char}</span>
)) }
function getCharClass(wordIdx, charIdx, char) {
if (wordIdx === currWordIndex && charIdx === currCharIndex && currChar && status !== 'finished') {
if (char === currChar) {
return 'has-background-success'
} else {
return 'has-background-danger'
}
} else if (wordIdx === currWordIndex && currCharIndex >= words[currWordIndex].length) {
return 'has-background-danger'
} else {
return ''
}
}
App.js
import {useState, useEffect, useRef} from 'react'
import randomWords from 'random-words'
const NUMB_OF_WORDS = 200
const SECONDS = 60
function App() {
const [words, setWords] = useState([])
const [countDown, setCountDown] = useState(SECONDS)
const [currInput, setCurrInput] = useState("")
const [currWordIndex, setCurrWordIndex] = useState(0)
const [currCharIndex, setCurrCharIndex] = useState(-1)
const [currChar, setCurrChar] = useState("")
const [correct, setCorrect] = useState(0)
const [incorrect, setIncorrect] = useState(0)
const [status, setStatus] = useState("waiting")
const textInput = useRef(null)
useEffect(() => {
setWords(generateWords())
}, [])
useEffect(() => {
if (status === 'started') {
textInput.current.focus()
}
}, [status])
function generateWords() {
return new Array(NUMB_OF_WORDS).fill(null).map(() => randomWords())
}
function start() {
if (status === 'finished') {
setWords(generateWords())
setCurrWordIndex(0)
setCorrect(0)
setIncorrect(0)
setCurrCharIndex(-1)
setCurrChar("")
}
if (status !== 'started') {
setStatus('started')
let interval = setInterval(() => {
setCountDown((prevCountdown) => {
if (prevCountdown === 0) {
clearInterval(interval)
setStatus('finished')
setCurrInput("")
return SECONDS
} else {
return prevCountdown - 1
}
} )
} , 1000 )
}
}
function handleKeyDown({keyCode, key}) {
// space bar
if (keyCode === 32) {
checkMatch()
setCurrInput("")
setCurrWordIndex(currWordIndex + 1)
setCurrCharIndex(-1)
// backspace
} else if (keyCode === 8) {
setCurrCharIndex(currCharIndex - 1)
setCurrChar("")
} else {
setCurrCharIndex(currCharIndex + 1)
setCurrChar(key)
}
}
function checkMatch() {
const wordToCompare = words[currWordIndex]
const doesItMatch = wordToCompare === currInput.trim()
if (doesItMatch) {
setCorrect(correct + 1)
} else {
setIncorrect(incorrect + 1)
}
}
function getCharClass(wordIdx, charIdx, char) {
if (wordIdx === currWordIndex && charIdx === currCharIndex && currChar && status !== 'finished') {
if (char === currChar) {
return 'has-background-success'
} else {
return 'has-background-danger'
}
} else if (wordIdx === currWordIndex && currCharIndex >= words[currWordIndex].length) {
return 'has-background-danger'
} else {
return ''
}
}
return (
<div className="App">
<div className="section">
<div className="is-size-1 has-text-centered has-text-primary">
<h2>{countDown}</h2>
</div>
</div>
<div className="control is-expanded section">
<input ref={textInput} disabled={status !== "started"} type="text" className="input" onKeyDown={handleKeyDown} value={currInput} onChange={(e) => setCurrInput(e.target.value)} />
</div>
<div className="section">
<button className="button is-info is-fullwidth" onClick={start}>
Start
</button>
</div>
{status === 'started' && (
<div className="section" >
<div className="card">
<div className="card-content">
<div className="content">
{words.map((word, i) => (
<span key={i}>
<span>
{word.split("").map((char, idx) => (
<span className={getCharClass(i, idx, char)} key={idx}>{char}</span>
)) }
</span>
<span> </span>
</span>
))}
</div>
</div>
</div>
</div>
)}
{status === 'finished' && (
<div className="section">
<div className="columns">
<div className="column has-text-centered">
<p className="is-size-5">Words per minute:</p>
<p className="has-text-primary is-size-1">
{correct}
</p>
</div>
<div className="column has-text-centered">
<p className="is-size-5">Accuracy:</p>
{correct !== 0 ? (
<p className="has-text-info is-size-1">
{Math.round((correct / (correct + incorrect)) * 100)}%
</p>
) : (
<p className="has-text-info is-size-1">0%</p>
)}
</div>
</div>
</div>
)}
</div>
);
}
export default App;
[…] Şimdi return() ifadesinde timer için bir başlık etiketi ekledik, ardından bir giriş alanı, bir düğme ekledik ve ondan sonrawords.map((word, i) => () harflerin arasına boşluk eklemek için.Devamını oku […]
[…] Posted on Nov 22 • Originally published at reactjsguru.com […]
[…] Теперь в операторе return() мы добавили тег заголовка для таймера, затем мы добавили поле ввода, кнопку и после этого у нас есть words.map((word, i) => (), чтобы добавить интервал между буквами.Читать далее […]