Letter game
Start the game by clicking the box and typing the letter shown. The clock starts and you get a new random letter to type as fast as you can. Each correct letter is followed by a new random letter, and if you type the wrong letter, the game ends. Go for it!
TL;DR
- A small typing game built in React with TypeScript.
useReducerkeeps the game state in one place, with the current mode asidleorrunning.useEffectreads the saved high score from local storage and picks the first random letter on mount.- A custom
useIntervalhook drives the running clock.
Try the finished game here:
0.00 s
The code behind the letter game
This game is built in React with TypeScript. The code is small but touches on several React building blocks I reach for in real apps.
useReducer
The game has two modes — idle before you start (or right after you've missed and a fresh letter is waiting) and running while you're typing. With several related fields that change together (time, current letter, correct count, score), useReducer is a better fit than scattering the state across multiple useState calls.
The shape of the state is described with TypeScript:
type GameState = "idle" | "running"
interface State {
state: GameState
time: number
letter: string
correct: number
score: number
highscore: number
}
type Action =
| { type: "loadLetter"; letter: string }
| { type: "correct"; letter: string }
| { type: "wrong"; letter: string; score: number; newHighscore?: number }
| { type: "tick" }
| { type: "loadHighscore"; highscore: number }
The Action type is a discriminated union, so each action carries exactly the data needed for that transition — and TypeScript narrows the type inside the reducer based on action.type.
function reducer(state: State, action: Action): State {
switch (action.type) {
case "loadLetter":
return { ...state, letter: action.letter }
case "correct":
if (state.state === "idle") {
return {
...state,
state: "running",
letter: action.letter,
time: 0,
correct: 1,
score: 0,
}
}
return {
...state,
letter: action.letter,
correct: state.correct + 1,
}
case "wrong":
return {
...state,
state: "idle",
letter: action.letter,
score: action.score,
highscore: action.newHighscore ?? state.highscore,
}
case "tick":
return { ...state, time: state.time + TIMER_FREQUENCY }
case "loadHighscore":
return { ...state, highscore: action.highscore }
}
}
The correct action handles two cases: if you're still in idle, the typed letter doubles as the start signal — the timer resets and the game flips to running. If you're already running, it just bumps the counter and shows the next letter. The wrong action drops back to idle and carries a fresh random letter, so the display is immediately ready for the next attempt.
In the component, the whole game state comes from one call:
const [state, dispatch] = useReducer(reducer, initialState)
State transitions are now expressed as dispatch({ type: "correct", letter: ... }) rather than juggling multiple setX calls — easier to reason about and easier to grep for.
useEffect
Another nice thing about React is useEffect. It lets us run side effects only when needed — in this case, reading the saved high score from local storage and picking the first random letter on mount.
useEffect(() => {
const saved = window.localStorage.getItem(STORAGE_KEY)
const parsed = Number(saved)
if (Number.isFinite(parsed) && parsed > 0) {
dispatch({ type: "loadHighscore", highscore: parsed })
}
dispatch({ type: "loadLetter", letter: getRandomLetter(alphabet) })
}, [])
The empty dependency array tells React this effect should only run once — when the component mounts. The Number.isFinite check makes the load robust against corrupted local-storage values. Picking the random letter inside useEffect (rather than in initialState) keeps the server-rendered HTML deterministic and avoids hydration mismatches with the client.
Custom hooks
useReducer and useEffect are examples of hooks built into React. You can also create your own. One piece of functionality I use fairly often is some kind of timing or having something happen on an interval. This can be done with the JavaScript function setInterval, but to make it a bit smoother I've extracted setInterval handling into a custom hook called useInterval. See more details in Custom hook: useInterval.
In the letter game, useInterval dispatches a tick action every 50 ms while the game is running:
useInterval(
() => dispatch({ type: "tick" }),
state.state === "running" ? TIMER_FREQUENCY : null,
)
Passing null as the delay tells useInterval to pause — so the timer is automatically active only during the running state, with no manual start/stop bookkeeping.
There you go — a small typing game and a tour of the React building blocks behind it!
References
- https://react.dev/reference/react/useReducer
- https://react.dev/reference/react/useEffect
- https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions
- https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
- https://developer.mozilla.org/en-US/docs/Web/API/setInterval
