What it tests
Owning an interval correctly in React — creating it, tearing it down, and never leaking one. It's the smallest problem that punishes a missing effect cleanup.
The approach
The interval is derived from running rather than started imperatively in a
click handler. The effect returns its own cleanup, so toggling or unmounting
clears the timer:
useEffect(() => {
if (!running) return;
const id = setInterval(() => setSeconds((s) => s + 1), 1000);
return () => clearInterval(id);
}, [running]);The functional update (s => s + 1) is what keeps seconds out of the
dependency array — reading it directly would need the effect to re-run every
tick.
Where it usually goes wrong
- No cleanup. Start, stop, start again and you have two intervals counting.
setSeconds(seconds + 1). Reads a stalesecondsfrom the effect's closure and sticks at 1.- Assuming
setIntervalis accurate. It drifts. A stopwatch that has to be correct should store a start timestamp and compute elapsed time fromDate.now(), using the interval only to trigger a re-render.
Full solution
1 file from src/folders/stopwatch.
index.jsx
import { useEffect, useState } from "react";
export function StopWatch() {
const [seconds, setSeconds] = useState(0);
const [running, setRunning] = useState(false);
useEffect(() => {
if (!running) return;
const id = setInterval(() => {
setSeconds((s) => s + 1);
}, 1000);
return () => clearInterval(id);
}, [running]);
const hh = String(Math.floor(seconds / 3600)).padStart(2, "0");
const mm = String(Math.floor((seconds % 3600) / 60)).padStart(2, "0");
const ss = String(seconds % 60).padStart(2, "0");
function handleReset() {
setSeconds(0);
setRunning(false);
}
return (
<div className="flex flex-col items-center justify-center gap-10">
<div className="text-2xl font-bold">
{hh}:{mm}:{ss}
</div>
<div className="flex gap-6">
<button onClick={() => setRunning(true)}>Start</button>
<button onClick={() => setRunning(false)}>Stop</button>
<button onClick={handleReset}>Reset</button>
</div>
</div>
);
}share this post