machine coding11
react ui components
useEffecttimerscleanup

Stopwatch

Build a stopwatch with start, stop and reset.

easy1 min readsolution

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 stale seconds from the effect's closure and sticks at 1.
  • Assuming setInterval is accurate. It drifts. A stopwatch that has to be correct should store a start timestamp and compute elapsed time from Date.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