Back to Machine Coding
Machine Coding
easy
mid

How would you build a stopwatch or timer component?

Track elapsed time from timestamps (not by counting ticks), drive UI updates with an interval or requestAnimationFrame, support start/pause/resume/reset/lap, clean up the timer on unmount, and stay accurate when the tab is backgrounded by computing elapsed = now - startTime.

5 min read·~20 min to think through

A stopwatch looks trivial but has a sharp accuracy trap. The key principle: derive elapsed time from timestamps, never accumulate it by counting ticks.

The accuracy trap

The naive approach — setInterval(() => setSeconds(s => s + 1), 1000)drifts. setInterval isn't precise, it's throttled when the tab is backgrounded, and errors accumulate. After a few minutes in a background tab, it's noticeably wrong.

Correct approach: store the start timestamp, and on each tick compute elapsed = Date.now() - startTime. The interval only drives re-renders; the value comes from real timestamps. Even if ticks are missed or delayed, the displayed time is always correct.

State & implementation

jsx
function Stopwatch() {
  const [elapsed, setElapsed] = useState(0);   // ms
  const [running, setRunning] = useState(false);
  const startRef = useRef(0);                  // when current run started
  const accumulatedRef = useRef(0);            // ms from previous runs (pause/resume)
  const intervalRef = useRef(null);

  const start = () => {
    startRef.current = Date.now();
    intervalRef.current = setInterval(() => {
      setElapsed(accumulatedRef.current + (Date.now() - startRef.current));
    }, 50); // 50ms interval => smooth display, value still from timestamps
    setRunning(true);
  };

  const pause = () => {
    clearInterval(intervalRef.current);
    accumulatedRef.current += Date.now() - startRef.current; // bank the run
    setRunning(false);
  };

  const reset = () => {
    clearInterval(intervalRef.current);
    accumulatedRef.current = 0;
    setElapsed(0);
    setRunning(false);
  };

  useEffect(() => () => clearInterval(intervalRef.current), []); // cleanup on unmount
  // ...render formatted elapsed, buttons
}

Features to cover

  • Start / Pause / ResumeaccumulatedRef banks completed runs so resume continues correctly.
  • Reset — clear interval + accumulated + elapsed.
  • Lap — push the current elapsed onto a laps array.
  • Formatting — ms → mm:ss.cs.

Key correctness points

  • CleanupclearInterval on unmount and on every stop, or you leak timers / setState on an unmounted component.
  • Timestamps, not tick-counting — accuracy survives background throttling.
  • useRef for the timer id and start/accumulated values — they shouldn't trigger re-renders; only elapsed should.
  • For sub-frame smoothness use requestAnimationFrame instead of setInterval (rAF pauses in background tabs — fine, since the value is timestamp-derived and corrects on return).
  • performance.now() over Date.now() if you want monotonic time unaffected by clock changes.

How to answer

"The trick is accuracy: don't count ticks with setInterval — it drifts and gets throttled in background tabs. Store the start timestamp and compute elapsed = now - start on each tick; the interval just drives re-renders. Use refs for the timer id and accumulated time so they don't cause renders, bank elapsed on pause for correct resume, and always clear the interval on unmount."

Follow-up questions

  • Why does counting ticks with setInterval drift?
  • How do you keep the stopwatch accurate when the tab is backgrounded?
  • Why use useRef for the timer id and start time instead of state?
  • When would you use requestAnimationFrame instead of setInterval?

Common mistakes

  • Counting ticks (s => s + 1) instead of computing elapsed from timestamps — drifts.
  • Not clearing the interval on unmount — timer leak / setState on unmounted component.
  • Storing the timer id in state, causing extra re-renders.
  • Pause/resume that loses or double-counts elapsed time.

Performance considerations

  • A ~50ms interval gives a smooth display without excessive renders. requestAnimationFrame aligns with paint and self-throttles in background tabs. Refs avoid re-rendering on timer-id/start-time changes.

Edge cases

  • Tab backgrounded for a long time, then resumed.
  • Rapid start/pause clicking.
  • System clock changing (use performance.now()).
  • Component unmounting while running.

Real-world examples

  • A workout timer, a chess clock, a task time-tracker.

Senior engineer discussion

Seniors immediately flag the tick-counting drift problem and derive elapsed time from timestamps so accuracy survives background throttling. They use refs for non-rendering values, handle pause/resume with a banked-accumulator, guarantee cleanup, and mention performance.now() and rAF as refinements.

Related questions