Bokstavsspel
Starta spelet genom att klicka på rutan och skriva bokstaven som visas. Då börjar tiden gå och du får en ny slumpmässig bokstav att skriva så snabbt som möjligt. Varje rätt följs av en ny slumpmässig bokstav och om du skriver fel tar spelet slut. Kör hårt!
TL;DR
- Ett litet snabbskrivningsspel byggt i React med TypeScript.
useReducerhåller spelets state samlat, med läget somidleellerrunning.useEffectläser in sparat rekord från local storage och slumpar fram första bokstaven vid mount.- En egen
useInterval-hook driver klockan.
Testa det färdiga spelet här:
0,00 s
Koden bakom bokstavsspelet
Det här spelet är byggt i React med TypeScript. Koden är liten men berör flera React-byggstenar jag använder i riktiga appar.
useReducer
Spelet har två lägen – idle innan du startar (eller direkt efter att du missat och en ny bokstav väntar) och running medan du spelar. Med flera relaterade fält som ändras tillsammans (tid, aktuell bokstav, antal rätt, poäng) passar useReducer bättre än att sprida ut staten över flera useState-anrop.
Statens form beskrivs med 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 }
Action-typen är en discriminated union, så varje action bär exakt den data som behövs för just den övergången – och TypeScript smalnar av typen inuti reducern baserat på 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 }
}
}
correct-actionen hanterar två fall: om du fortfarande är i idle fungerar den första rätta bokstaven som startsignal – tiden nollställs och spelet flippar över till running. Om du redan är running räknar den bara upp poängen och plockar fram nästa bokstav. wrong-actionen släpper tillbaka till idle och bär med sig en ny slumpbokstav, så displayen är direkt redo för nästa försök.
I komponenten kommer hela spelets state från ett enda anrop:
const [state, dispatch] = useReducer(reducer, initialState)
Övergångar i staten uttrycks nu som dispatch({ type: "correct", letter: ... }) istället för att jonglera flera setX-anrop – lättare att resonera om och lättare att grepa efter.
useEffect
En annan trevlig sak med React är useEffect. Den låter oss köra sidoeffekter bara när det behövs – i det här fallet att läsa sparat rekord från local storage och plocka första slumpbokstaven vid 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) })
}, [])
Den tomma dependency-arrayen säger till React att den här effekten bara ska köras en gång – när komponenten mountas. Number.isFinite-kontrollen gör inläsningen robust mot korrupta local-storage-värden. Att plocka slumpbokstaven inuti useEffect (i stället för i initialState) håller den serverrenderade HTML:en deterministisk och undviker hydration mismatch mot klienten.
Egna hooks
useReducer och useEffect är exempel på hooks som är inbyggda i React. Det går också att skapa egna. En funktionalitet som jag använder ganska ofta är någon form av tidtagning eller att någonting ska hända enligt ett visst tidsintervall. Detta går att åstadkomma med JavaScript-funktionen setInterval, men för att göra det lite smidigare har jag brutit ut hanteringen av setInterval i en egen hook som jag kallar useInterval. Mer detaljer finns i Custom hook: useInterval.
I bokstavsspelet skickar useInterval en tick-action var 50:e ms medan spelet är igång:
useInterval(
() => dispatch({ type: "tick" }),
state.state === "running" ? TIMER_FREQUENCY : null,
)
Att skicka in null som delay säger till useInterval att pausa – så timern är automatiskt aktiv bara under running-läget, utan att vi själva behöver hålla reda på start och stopp.
Så där, ett litet snabbskrivningsspel och en rundtur i React-byggstenarna bakom det!
Referenser
- 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
