Build the arcade game: a mole pops up in a random hole, and clicking it before it disappears scores a point. The mechanics are tiny — one piece of state for which hole the mole is in, a setInterval that relocates it on a timer, and a click handler that scores only when you hit the right hole.
// A self-contained component. No props.
function App(): JSX.Element;
A grid of holes, a score, and a mole that jumps around.
mole at hole 4 → click hole 4 → score +1, mole jumps elsewhere
mole at hole 4 → click hole 1 → nothing (missed)
every ~800ms with no click, the mole moves to a new random hole
setInterval in an effect; clear it on unmount so it doesn't leak.Two pieces of state — the mole's hole index and the score — plus a setInterval that relocates the mole on a timer. A hole shows the mole when its index matches; clicking that hole scores and moves it.
It looks like a game, but the moving parts are minimal. There's exactly one mole, so you only need to remember which hole it's in. A timer makes it jump to a random hole periodically. Clicking is a hit only when you click the hole the mole is currently in. The two things people get wrong are leaking the interval (not clearing it) and reading stale state inside the interval — both fixed by setting up the timer once in an effect and using functional updates.
mole is the index of the occupied hole; score is a number. A useEffect with [] starts a setInterval that sets mole to a random index every ~800ms, and its cleanup clears the interval. Each hole renders the mole (an emoji) when its index equals mole. whack(i) checks i === mole: if so, score + 1 and immediately relocate the mole; otherwise nothing.
A first attempt starts the interval in the render body, or recreates it on every render:
setInterval(() => setMole(rand()), 800); // new interval every render → many moles, leaks
Calling setInterval during render (or in an effect without []) stacks up a new timer on every render, so the mole flickers wildly and the timers never get cleared. The fix is one effect with an empty dependency array that creates the interval once and clears it on unmount.
import { useState, useEffect } from 'react';
import './styles.css';
const HOLES = [0, 1, 2, 3, 4, 5, 6, 7, 8];
function randomHole() {
return Math.floor(Math.random() * HOLES.length);
}
export default function App() {
const [mole, setMole] = useState(randomHole);
const [score, setScore] = useState(0);
useEffect(() => {
const id = setInterval(() => setMole(randomHole()), 800);
return () => clearInterval(id);
}, []);
function whack(i: number) {
if (i !== mole) return; // missed
setScore((s) => s + 1);
setMole(randomHole());
}
return (
<main className="container">
<h1>Whack-A-Mole</h1>
<p className="score">Score: {score}</p>
<div className="grid">
{HOLES.map((i) => (
<button
key={i}
className={i === mole ? 'hole up' : 'hole'}
onClick={() => whack(i)}
>
{i === mole ? '🐹' : ''}
</button>
))}
</div>
</main>
);
}
mole is initialised lazily with useState(randomHole) (passing the function, not calling it, so it runs once). The effect creates a single interval and returns clearInterval so it's torn down on unmount — no leak, no stacking. whack early-returns on a miss; on a hit it bumps the score with a functional update (s => s + 1) and relocates the mole right away, so you can't double-score the same mole. Each hole gets the up class and the 🐹 only when it's the occupied one.
Say mole = 4, score = 0.
up class; the rest are empty. Score 0.whack(4): 4 === mole → setScore(s => s + 1) (score 1) and setMole(randomHole()) (say 7). Hole 4 empties, hole 7 shows the mole.setMole(randomHole()) → the mole jumps again on its own.whack(i) with i !== mole → early return, nothing happens.clearInterval(id) — the timer stops.[]. Stacks timers every render → flicker + leak. Fix: one effect, empty deps.return () => clearInterval(id).setMole(Math.random()*9) without floor. Produces a fractional index that matches nothing. Fix: Math.floor.i === mole guard scores on misses. Fix: early-return on a miss.score in the interval. If the interval read score it'd be stale; here only the click updates score, with a functional update.import { useEffect, useReducer } from 'react';
import './styles.css';
const HOLES = [0, 1, 2, 3, 4, 5, 6, 7, 8];
function randomHole() {
return Math.floor(Math.random() * HOLES.length);
}
type State = { mole: number; score: number };
type Action = { type: 'tick' } | { type: 'whack'; index: number };
function reducer(state: State, action: Action): State {
if (action.type === 'tick') return { ...state, mole: randomHole() };
if (action.index !== state.mole) return state;
return { mole: randomHole(), score: state.score + 1 };
}
export default function App() {
const [game, dispatch] = useReducer(reducer, undefined, () => ({
mole: randomHole(),
score: 0,
}));
useEffect(() => {
const timer = setInterval(() => dispatch({ type: 'tick' }), 800);
return () => clearInterval(timer);
}, []);
return (
<main className="container">
<h1>Whack-A-Mole</h1>
<p className="score">Score: {game.score}</p>
<div className="grid">
{HOLES.map((index) => (
<button
key={index}
className={index === game.mole ? 'hole up' : 'hole'}
onClick={() => dispatch({ type: 'whack', index })}
>
{index === game.mole ? '🐹' : ''}
</button>
))}
</div>
</main>
);
}The reducer makes misses true no ops while keeping timer and click transitions in one testable state machine.
import { useEffect, useState } from 'react';
import './styles.css';
const HOLES = [0, 1, 2, 3, 4, 5, 6, 7, 8];
function randomHole() {
return Math.floor(Math.random() * HOLES.length);
}
function useMoleGame() {
const [mole, setMole] = useState(randomHole);
const [score, setScore] = useState(0);
useEffect(() => {
const timer = setInterval(() => setMole(randomHole()), 800);
return () => clearInterval(timer);
}, []);
function whack(index: number) {
if (index !== mole) return;
setScore((current) => current + 1);
setMole(randomHole());
}
return { mole, score, whack };
}
export default function App() {
const game = useMoleGame();
return (
<main className="container">
<h1>Whack-A-Mole</h1>
<p className="score">Score: {game.score}</p>
<div className="grid">
{HOLES.map((index) => (
<button
key={index}
className={index === game.mole ? 'hole up' : 'hole'}
onClick={() => game.whack(index)}
>
{index === game.mole ? '🐹' : ''}
</button>
))}
</div>
</main>
);
}The component only renders the board while the custom hook owns lifecycle cleanup and game rules.
Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
Build the arcade game: a mole pops up in a random hole, and clicking it before it disappears scores a point. The mechanics are tiny — one piece of state for which hole the mole is in, a setInterval that relocates it on a timer, and a click handler that scores only when you hit the right hole.
// A self-contained component. No props.
function App(): JSX.Element;
A grid of holes, a score, and a mole that jumps around.
mole at hole 4 → click hole 4 → score +1, mole jumps elsewhere
mole at hole 4 → click hole 1 → nothing (missed)
every ~800ms with no click, the mole moves to a new random hole
setInterval in an effect; clear it on unmount so it doesn't leak.Two pieces of state — the mole's hole index and the score — plus a setInterval that relocates the mole on a timer. A hole shows the mole when its index matches; clicking that hole scores and moves it.
It looks like a game, but the moving parts are minimal. There's exactly one mole, so you only need to remember which hole it's in. A timer makes it jump to a random hole periodically. Clicking is a hit only when you click the hole the mole is currently in. The two things people get wrong are leaking the interval (not clearing it) and reading stale state inside the interval — both fixed by setting up the timer once in an effect and using functional updates.
mole is the index of the occupied hole; score is a number. A useEffect with [] starts a setInterval that sets mole to a random index every ~800ms, and its cleanup clears the interval. Each hole renders the mole (an emoji) when its index equals mole. whack(i) checks i === mole: if so, score + 1 and immediately relocate the mole; otherwise nothing.
A first attempt starts the interval in the render body, or recreates it on every render:
setInterval(() => setMole(rand()), 800); // new interval every render → many moles, leaks
Calling setInterval during render (or in an effect without []) stacks up a new timer on every render, so the mole flickers wildly and the timers never get cleared. The fix is one effect with an empty dependency array that creates the interval once and clears it on unmount.
import { useState, useEffect } from 'react';
import './styles.css';
const HOLES = [0, 1, 2, 3, 4, 5, 6, 7, 8];
function randomHole() {
return Math.floor(Math.random() * HOLES.length);
}
export default function App() {
const [mole, setMole] = useState(randomHole);
const [score, setScore] = useState(0);
useEffect(() => {
const id = setInterval(() => setMole(randomHole()), 800);
return () => clearInterval(id);
}, []);
function whack(i: number) {
if (i !== mole) return; // missed
setScore((s) => s + 1);
setMole(randomHole());
}
return (
<main className="container">
<h1>Whack-A-Mole</h1>
<p className="score">Score: {score}</p>
<div className="grid">
{HOLES.map((i) => (
<button
key={i}
className={i === mole ? 'hole up' : 'hole'}
onClick={() => whack(i)}
>
{i === mole ? '🐹' : ''}
</button>
))}
</div>
</main>
);
}
mole is initialised lazily with useState(randomHole) (passing the function, not calling it, so it runs once). The effect creates a single interval and returns clearInterval so it's torn down on unmount — no leak, no stacking. whack early-returns on a miss; on a hit it bumps the score with a functional update (s => s + 1) and relocates the mole right away, so you can't double-score the same mole. Each hole gets the up class and the 🐹 only when it's the occupied one.
Say mole = 4, score = 0.
up class; the rest are empty. Score 0.whack(4): 4 === mole → setScore(s => s + 1) (score 1) and setMole(randomHole()) (say 7). Hole 4 empties, hole 7 shows the mole.setMole(randomHole()) → the mole jumps again on its own.whack(i) with i !== mole → early return, nothing happens.clearInterval(id) — the timer stops.[]. Stacks timers every render → flicker + leak. Fix: one effect, empty deps.return () => clearInterval(id).setMole(Math.random()*9) without floor. Produces a fractional index that matches nothing. Fix: Math.floor.i === mole guard scores on misses. Fix: early-return on a miss.score in the interval. If the interval read score it'd be stale; here only the click updates score, with a functional update.import { useEffect, useReducer } from 'react';
import './styles.css';
const HOLES = [0, 1, 2, 3, 4, 5, 6, 7, 8];
function randomHole() {
return Math.floor(Math.random() * HOLES.length);
}
type State = { mole: number; score: number };
type Action = { type: 'tick' } | { type: 'whack'; index: number };
function reducer(state: State, action: Action): State {
if (action.type === 'tick') return { ...state, mole: randomHole() };
if (action.index !== state.mole) return state;
return { mole: randomHole(), score: state.score + 1 };
}
export default function App() {
const [game, dispatch] = useReducer(reducer, undefined, () => ({
mole: randomHole(),
score: 0,
}));
useEffect(() => {
const timer = setInterval(() => dispatch({ type: 'tick' }), 800);
return () => clearInterval(timer);
}, []);
return (
<main className="container">
<h1>Whack-A-Mole</h1>
<p className="score">Score: {game.score}</p>
<div className="grid">
{HOLES.map((index) => (
<button
key={index}
className={index === game.mole ? 'hole up' : 'hole'}
onClick={() => dispatch({ type: 'whack', index })}
>
{index === game.mole ? '🐹' : ''}
</button>
))}
</div>
</main>
);
}The reducer makes misses true no ops while keeping timer and click transitions in one testable state machine.
import { useEffect, useState } from 'react';
import './styles.css';
const HOLES = [0, 1, 2, 3, 4, 5, 6, 7, 8];
function randomHole() {
return Math.floor(Math.random() * HOLES.length);
}
function useMoleGame() {
const [mole, setMole] = useState(randomHole);
const [score, setScore] = useState(0);
useEffect(() => {
const timer = setInterval(() => setMole(randomHole()), 800);
return () => clearInterval(timer);
}, []);
function whack(index: number) {
if (index !== mole) return;
setScore((current) => current + 1);
setMole(randomHole());
}
return { mole, score, whack };
}
export default function App() {
const game = useMoleGame();
return (
<main className="container">
<h1>Whack-A-Mole</h1>
<p className="score">Score: {game.score}</p>
<div className="grid">
{HOLES.map((index) => (
<button
key={index}
className={index === game.mole ? 'hole up' : 'hole'}
onClick={() => game.whack(index)}
>
{index === game.mole ? '🐹' : ''}
</button>
))}
</div>
</main>
);
}The component only renders the board while the custom hook owns lifecycle cleanup and game rules.
Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.