Creating Typeracer Game Using ReactJS — Final Part

Mario Gunawan
6 min readJan 15, 2023

--

Hello! If you haven’t read part 1 & 2 yet, read it first! You can read it here (part 1).

Welcome back to the 3rd and last part of the typeracer series! In previous parts, we’ve created:

  • States & watchers that keep track of whether the quote is completed or not
  • Autofocus on input
  • Coloring for the user’s current word

Here’s what we have built in our previous sessions:

Now we’ll build stats to track how well we are doing in a game. Let’s go!

Code Cleanup

Before we move on to this part, we have some cleanups to do. Previously, we bundled the function randomQuote and type Quotewithin index.tsx. We usually separate those functions/types from the UI file because it’s easier to read the code that way. So let’s create a new file, repository.ts and move the Quote type and randomQuote function to the file.

// repository.ts
import quotes from "./quotes.json";

export type Quote = {
quote: string;
movieName: string;
};

export const randomQuote = (): Quote =>
quotes[Math.floor(quotes.length * Math.random())];

Then, you can remove the function & type from index.tsx and add in the import:

// index.tsx
import { Quote, randomQuote } from "./repository";

Usually, we separate the model Quote and the function randomQuote even further, but it’s good enough right now.

The UI

Like previous parts, since the UI isn’t an important part of the app, you can copy and paste the code or customize it yourself.

Create a new file called stats_display.dart which is responsible to display stats & have a next button. Copy & paste the below code to the file:

// stats_display.dart
import { FC } from 'react'
import { Quote } from './repository'

interface StatsDisplayProps {
quote: Quote
endTime: number
startTime: number
numOfWords: number
onClickNextQuote: () => void
}

const StatsDisplay: FC<StatsDisplayProps> = ({
quote,
startTime,
endTime,
numOfWords,
onClickNextQuote,
}) => {
const typeDurationInSeconds = (endTime - startTime) / 1000
const wps = numOfWords / typeDurationInSeconds
const wpm = Math.floor(wps * 60)

return (
<div className="w-full border rounded-xl p-8">
<p className="text-xl font-bold font-sans">
You just typed a quote from {quote.movieName}
</p>
<p className="mt-2">Your stats:</p>
<ul>
<li>Words Per Minute: {wpm}</li>
<li>Words Per Second: {wps.toFixed(2)}</li>
</ul>
<button
onClick={onClickNextQuote}
id="next_quote_button"
className="px-4 py-2 border rounded-xl bg-blue-500 text-white mt-4"
>
Next Quote
</button>
</div>
)
}

export default StatsDisplay

in StatsDisplay, we’re passing the time startTime and endTime which represents the time the player started typing and the time the player finishes it. We also passes the quote to extract its source (movieName) and onClickNextQuote which is a callback when we click the “Next Quote” button.

Now add StatsDisplay to index.tsx page below the input element:

// index.tsx
// ...
<input /* ... */ />
{quote && (
<StatsDisplay
startTime={0}
endTime={1000}
quote={quote}
numOfWords={quotesSplit.length}
onClickNextQuote={() => {}}
/>
)}
// ...

When you save the app and reload, this should be what you see:

It looks pretty good, right? Now there’s one more step to do: make the StatsDisplay functionality

Adding GameState

Create a new file, called game_state.ts then add in those lines:

export const GameState = {
PLAYING: "PLAYING",
VIEW_STATS: "VIEW_STATS",
WAITING: "WAITING",
};

The GameState variable is for determining the state of the game. Here is the GameState lifecycle:

  1. Starts at WAITING while we are loading the quote
  2. After the quote is loaded, game state changes to PLAYING
  3. After finished typing, it goes into VIEW_STATS
  4. Finally, when the player clicks Next button, it goes into PLAYING again

After that, in index.tsx , create a string state that contains the current game state below the correctGreenWord state:


const correctGreenWord = useMemo(() => { /* ... */ });
const [gameState, setGameState] = useState(GameState.WAITING);
// ...

then, in the line where we do setQuote , change it so that it changes the gameState to GameState.PLAYING :

// replace this: 
/*

useEffect(() => {
if (wordIdx == quotesSplit.length) {
setQuote(randomQuote());
}
}, [wordIdx, quotesSplit]);

*/

// to this:
useEffect(() => {
setGameState(GameState.PLAYING);
}, []);

useEffect(() => {
if (gameState == GameState.PLAYING) {
setQuote(randomQuote());
}
}, [gameState]);

After that, we set gameState=VIEW_STATS when the player have finished typing:

useEffect(() => {
const quoteFinished =
quotesSplit.length == wordIdx && quotesSplit.length != 0;
if (quoteFinished) {
setGameState(GameState.VIEW_STATS);
}
}, [wordIdx, quotesSplit]);

Finally, let’s implement the onClickNextQuote functionality by creating a new method called nextQuote . Below is the changes:

// ...

const nextQuote = () => {
setGameState(GameState.PLAYING);
};

return (
// ...
{quote && gameState == GameState.VIEW_STATS && (
<StatsDisplay
startTime={0}
endTime={1000}
quote={quote}
numOfWords={quotesSplit.length}
onClickNextQuote={nextQuote} // change this line
/>
)}
)
// ...

This should be the current state of our app:

My typing speed looks pretty slow here, I promise you it’s actually faster than this!

Adding Start & End Time

We can look stats, but the word per minute and word per second are still an arbitrary number. Let’s fix that by creating 2 state, startTime, and endTime

const [startTime, setStartTime] = useState<number>(0)
const [endTime, setEndTime] = useState<number>(0)

Let’s set the startTime when the game begin ( gameState==PLAYING ) and set endTime when the the player finished typing( gameState==VIEW_STATS ). Also, change the StatsDisplay so that it uses the state instead of arbitrary number:


useEffect(() => {
if (gameState == GameState.PLAYING) {
setQuote(randomQuote());
setStartTime(Date.now()); // add this line to an existing useEffect
}
if (gameState == GameState.VIEW_STATS) {
setEndTime(Date.now()); // add this line to an existing useEffect
}
}, [gameState]);

return (
// ...
{quote && gameState == GameState.VIEW_STATS && (
<StatsDisplay
startTime={startTime}
endTime={endTime}
quote={quote}
numOfWords={quotesSplit.length}
onClickNextQuote={nextQuote}
/>
)}
// ...
)

Now, we got some real Word Per Minute (WPM) / Word Per Second (WPS)!

Focusing Input After A Quote

The main functionality is done and all, but it’s annoying to click the input bar every time we’re done with a quote, don’t you agree? So, let’s fix that problem by moving the input.focus part to when gameState == PLAYING :

// Previously this:
/*
useEffect(() => {
document.getElementById(inputId)?.focus();
}, []);
*/

// move it to this part:
useEffect(() => {
if (gameState == GameState.PLAYING) {
document.getElementById(inputId)?.focus(); // add this line
setQuote(randomQuote());
setStartTime(Date.now());
}
if (gameState == GameState.VIEW_STATS) {
setEndTime(Date.now());
}
}, [gameState]);

That’s it, now our input is focused while we’re playing the game.

Disabling Input When Viewing Stats

This is the last part. Currently, our input is not disabled when the player is viewing stats. It should be disabled because the quote is finished. So, let’s change our code inside the <input … > tag to be disabled while viewing stats:

<input
className="w-full border-black border px-4 py-2"
onChange={(text) => setText(text.target.value)}
value={text}
disabled={gameState == GameState.VIEW_STATS}
id={inputId}
/>

Tadah! It’s disabled when we’re viewing stats:

Final Code

You can see here for the finished index.tsx code in our simple typeracer game. Also, here is the full repository for the game.

Afterthoughts

After a bit of self review and some days in between editing this code/article, I have realized something… that this code is really dirty. I will not clean it right now, since I’m currently finishing up my renewed portfolio website, but it might be done in the future for content :).

Anyway, thanks for reading! I hope this was helpful for you in some ways, or at least it was an interesting read.

--

--

Mario Gunawan
Mario Gunawan

Written by Mario Gunawan

I'm a mobile / web developer. I'm mostly writing articles about software development.

No responses yet