This interview question was asked in MakeMyTrip SDE2 frontend machine coding round.
The problem statement reads as: Typeracer is a game to improve typing speed.
- The UI will have a screen where a random paragraph is there. The player has to type the words in the paragraph.
- All characters are black initially.
- Each correctly word character will turn green in color.
- Each wrongly word character will turn red and all the subsequent characters will also turn red.
- When all characters are word correctly, typing speed is displayed in words per minute and the game ends.
Based on the problem it is clear that we have to create a typing test web app like MonkeyType.
Let us see how we can implement the same in React.
Generating the paragraph
Breaking down the problem statement, I will first render the each character of the paragraph as a <span>i<span/> so that we can track and then mark it as valid, invalid, or not-typed.
I am using this txtgen package that provides random paragraph of the specified size.
npm i txtgen
We will generate this paragraph globally to avoid getting a new list on state update and then create an array of characters from this paragraph by splitting it and wrapping each character in span.
import { paragraph } from "txtgen";
const pg = paragraph([1]);
const lettersToHTML = (pg) => {
const letters = pg.split("");
return letters.map((e, i) => {
return <span key={e + i}>{e}</span>;
});
};
export default function App() {
return (
<div className="App">
<p className="paragraph">{lettersToHTML(pg)}</p>
</div>
);
}
.paragraph {
font-size: 1.5em;
}
This will generate each character from the paragraph in a individual span to which we can add classes to change its color and individually track it.

Highlighting the characters in paragraph
Next is we will listen to the keydown event and track which key is pressed. We will store the character in the state and then pass this state to the lettersToHTML function to check if the right character is added or not and then accordingly added classes to highlight them.
We will also add a blink cursor to characters span to showcase at which character we are at currently.
import { paragraph } from "txtgen";
import { useState, useEffect } from "react";
import "./styles.css";
const pg = paragraph([1]);
const lettersToHTML = (pg, typedText = []) => {
const letters = pg.split("");
return letters.map((e, i) => {
// get the current typed character and check if the matches
const currentTypedCharacter = typedText[i];
const valid = currentTypedCharacter?.toLowerCase() === e?.toLowerCase();
const className = (() => {
// base class when no character is typed
if (typedText.length === 0) {
return "black";
}
// Check for subseqeuent characters
if (typedText.length <= i) {
// This checks if the last typed character matches the on the original characters
const lastTypedText = typedText[typedText.length - 1];
const ogCharacterAtLastTypedTextIndex = letters[typedText.length - 1];
const lastTypedCharacterInvalid =
lastTypedText?.toLowerCase() !==
ogCharacterAtLastTypedTextIndex?.toLowerCase();
// if last typed character does not matches then make all the subseqeuent invalid
return lastTypedCharacterInvalid ? "invalid" : "black";
}
// check if valid or not
return valid ? "valid" : "invalid";
})();
// show blink before the current character
const showBlink = i === typedText.length;
return (
<span
key={e + i}
className={showBlink ? className + " blink" : className}
>
{e}
</span>
);
});
};
export default function App() {
const [str, setStr] = useState([]);
useEffect(() => {
const onType = (e) => {
if (e.key === "Backspace") {
// remove the last character on each Backspace
setStr((prev) => {
prev.pop();
return [...prev];
});
} else {
// add to characters array
setStr((prev) => [...prev, e.key]);
}
};
window.addEventListener("keydown", onType);
return () => {
window.removeEventListener("keydown", onType);
};
}, []);
return (
<div className="App">
<p className="paragraph">{lettersToHTML(pg, str)}</p>
</div>
);
}
.invalid {
color: hsl(0, 100%, 50%);
background-color: rgba(255, 0, 0, 0.2);
}
.valid {
color: green;
}
.paragraph {
font-size: 1.5em;
}
@keyframes blink {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.blink::before {
content: "";
display: inline-block;
height: 20px;
width: 3px;
background-color: #fd3d7c;
animation-name: blink;
animation-duration: 1s;
animation-iteration-count: infinite;
animation-timing-function: ease-in-out;
}
This handles all the cases like:
- All characters are black initially.
- Each correctly word character will turn green in color.
- Each wrongly word character will turn red and all the subsequent characters will also turn red.

For the third point, we have added check for lastTypedCharacterInvalid which if is invalid we highlight the subsequent characters.
Doing the calculation of word per minute
The final part pending is adding the calculation for word per minute. For this, we will maintain two separate states for start-time and end-time.
We will add a button on whose click the start-time will be set and then only we can start the typing. This button will reset the state once the typing is concluded.
const onStartClick = () => {
// if no endTime then start
if (!endTime) {
setStartTime(Date.now());
}
// reset if endTime is added
// endTime is updated when the typing is completed
// thus this is a safe check
else {
setStartTime(Date.now());
setEndTime(0);
setStr([]);
}
};
To check if the typing is concluded we will check if the typed characters length and the paragraph length are same and there are no mismatched characters.
// check if there is any mismatch characters
const mismatchCharacters = str.some((e, i) => {
return e.toLowerCase() !== pg[i].toLowerCase();
});
// check if typing is completed
const completedTyping = pg.length === str.length && !mismatchCharacters;
// if typing is completed update the end time
useEffect(() => {
if (completedTyping) {
setEndTime(Date.now());
}
}, [completedTyping]);
And the finally update the condition on the keydown event listener to only invoke if the typing has started and it is not concluded.
useEffect(() => {
const onType = (e) => {
// base condition: terminate if typing is not started or it is concluded
if (!startTime || completedTyping) {
return;
}
if (e.key === "Backspace") {
// remove the last character on each Backspace
setStr((prev) => {
prev.pop();
return [...prev];
});
} else {
// add to characters array
setStr((prev) => [...prev, e.key]);
}
};
window.addEventListener("keydown", onType);
return () => {
window.removeEventListener("keydown", onType);
};
}, [startTime, completedTyping]);
Then we will pass the words and the start-time and the end-time to the helper function that will calculate word typed per minute.
function calculateWPM(text, startTime, endTime) {
// Count words by splitting on whitespace
const wordCount = text.trim().split(/\s+/).length;
// Time in minutes
const timeInMinutes = (endTime - startTime) / 1000 / 60;
// Avoid division by zero
if (timeInMinutes === 0) return 0;
// Calculate WPM
const wpm = wordCount / timeInMinutes;
return Math.round(wpm);
}
Final code
Putting everything together
import "./styles.css";
import { useEffect, useState } from "react";
import { paragraph } from "txtgen";
const pg = paragraph([1]);
function calculateWPM(text, startTime, endTime) {
// Count words by splitting on whitespace
const wordCount = text.trim().split(/\s+/).length;
// Time in minutes
const timeInMinutes = (endTime - startTime) / 1000 / 60;
// Avoid division by zero
if (timeInMinutes === 0) return 0;
// Calculate WPM
const wpm = wordCount / timeInMinutes;
return Math.round(wpm);
}
const lettersToHTML = (pg, typedText = []) => {
const letters = pg.split("");
return letters.map((e, i) => {
// get the current typed character and check if the matches
const currentTypedCharacter = typedText[i];
const valid = currentTypedCharacter?.toLowerCase() === e?.toLowerCase();
const className = (() => {
// base class when no character is typed
if (typedText.length === 0) {
return "black";
}
// Check for subseqeuent characters
if (typedText.length <= i) {
// This checks if the last typed character matches the on the original characters
const lastTypedText = typedText[typedText.length - 1];
const ogCharacterAtLastTypedTextIndex = letters[typedText.length - 1];
const lastTypedCharacterInvalid =
lastTypedText?.toLowerCase() !==
ogCharacterAtLastTypedTextIndex?.toLowerCase();
// if last typed character does not matches then make all the subseqeuent invalid
return lastTypedCharacterInvalid ? "invalid" : "black";
}
// check if valid or not
return valid ? "valid" : "invalid";
})();
// show blink before the current character
const showBlink = i === typedText.length;
return (
<span
key={e + i}
className={showBlink ? className + " blink" : className}
>
{e}
</span>
);
});
};
export default function App() {
// track time and character typed
const [startTime, setStartTime] = useState(0);
const [endTime, setEndTime] = useState(0);
const [str, setStr] = useState([]);
// check if there is any mismatch characters
const mismatchCharacters = str.some((e, i) => {
return e.toLowerCase() !== pg[i].toLowerCase();
});
// check if typing is completed
const completedTyping = pg.length === str.length && !mismatchCharacters;
useEffect(() => {
const onType = (e) => {
// base condition: terminate if typing is not started or it is concluded
if (!startTime || completedTyping) {
return;
}
if (e.key === "Backspace") {
// remove the last character on each Backspace
setStr((prev) => {
prev.pop();
return [...prev];
});
} else {
// add to characters array
setStr((prev) => [...prev, e.key]);
}
};
window.addEventListener("keydown", onType);
return () => {
window.removeEventListener("keydown", onType);
};
}, [startTime, completedTyping]);
// if typing is completed update the end time
useEffect(() => {
if (completedTyping) {
setEndTime(Date.now());
}
}, [completedTyping]);
const onStartClick = () => {
// if no endTime then start
if (!endTime) {
setStartTime(Date.now());
}
// reset if endTime is added
// endTime is updated when the typing is completed
// thus this is a safe check
else {
setStartTime(Date.now());
setEndTime(0);
setStr([]);
}
};
return (
<div className="App">
<button onClick={onStartClick}>
{endTime ? "Reset" : startTime ? "Started" : "Start"}
</button>
<p className="paragraph">{lettersToHTML(pg, str)}</p>
{completedTyping && <p>WPM: {calculateWPM(pg, startTime, endTime)}</p>}
</div>
);
}

Note:- For simplicity I have done the case-insensitive check by converting both to the lower case, if you want you can remove this add the case sensitive check as well but for that you will need to handle the shift button press in the keydown event listener.