Back

Stopwatch

easy

Streak

0 days

Progress

0%

Submitted

0

Stopwatch

React30 mineasyFree

Prompt

A stopwatch is one of the most deceptively tricky components in frontend interviews. It looks simple — start, stop, reset — but the moment you reach for useState to track elapsed time with setInterval, you run into stale closure bugs that silently produce wrong results.

Build a fully functional stopwatch displaying elapsed time in MM:SS.cs format. Users can start, pause, and reset. The display must update at ~10ms intervals without memory leaks when the component unmounts.

The core challenge is timer management: use useRef to anchor to wall-clock time via Date.now() instead of accumulating ticks. This is a perfect question for demonstrating senior-level React timer patterns.

Requirements

  • →Display elapsed time as MM:SS.cs (centiseconds)
  • →Start button begins or resumes the timer
  • →Pause button stops without resetting
  • →Reset button stops and zeroes the display
  • →No memory leaks — clear interval on unmount
  • →Use wall-clock anchoring, not tick accumulation
Example
Loading preview...
For the best coding experience, we recommend using a desktop device.
Preparing Sandbox...
Premium interview report

What interviewers score in this build

Use this before reading the code. It tells you what to say, what to test, and where machine-coding candidates usually lose points.

Interview signals

  • Wall-clock anchoring: Uses Date.now() - startTime instead of accumulating ticks
  • Ref vs State discipline: Interval ID and timestamp in refs, only display in state
  • Pause/Resume correctness: Correctly offsets anchor on resume using savedRef
  • Memory leak prevention: useEffect cleanup clears interval on unmount

Time checkpoints

  1. 1

    0–3 min: Explain stale closure problem and wall-clock approach

  2. 2

    3–7 min: Set up refs for intervalId and startTime

  3. 3

    7–12 min: Implement start/pause/reset with savedRef offset

  4. 4

    12–16 min: Write fmt() helper for MM:SS.cs

Edge-case checklist

Start/stop/reset
Unmount while running
Lap if in scope

Common mistakes

  • setState every ms without care
  • Timer leak
  • Clock drift from careless Date.now usage
SolutionRead-only · Live Preview

Technical Explanation

Why This Question Exists

Interviewers use the Stopwatch to test one specific thing: do you understand stale closures in React? Almost every candidate reaches for setInterval(() => setTime(t => t + 100), 100) first. That looks reasonable — but it silently drifts because setInterval doesn't fire at exactly the interval you specify. Over 60 seconds you can lose 1–2 seconds of accuracy.

The second trap is the stale closure. If you write setInterval(() => setElapsed(elapsed + 10), 10), the closure captures the initial value of elapsed (zero) and never re-reads it. Every tick adds 10 to zero. The display freezes at 10ms. This bug is invisible — no error, wrong output.

The Wrong Approach (and why it fails)

// ❌ WRONG — stale closure bug
const [elapsed, setElapsed] = useState(0);
useEffect(() => {
  const id = setInterval(() => {
    setElapsed(elapsed + 10); // 'elapsed' is always 0 here
  }, 10);
  return () => clearInterval(id);
}, []); // [] means closure never updates

The empty dependency array means the effect only runs once. The elapsed inside the callback is permanently captured as 0. Even with the functional updater form setElapsed(e => e + 10), you're still subject to timer drift over time.

The Right Approach: Wall-Clock Anchoring

// ✅ CORRECT — wall-clock anchoring
const startRef = useRef(0);   // wall-clock when started
const savedRef = useRef(0);   // accumulated ms before last pause

const start = () => {
  startRef.current = Date.now() - savedRef.current;
  intervalRef.current = setInterval(() => {
    const elapsed = Date.now() - startRef.current;
    savedRef.current = elapsed;
    setDisplay(elapsed); // only state needed for render
  }, 10);
};

The key: Date.now() - startRef.current always gives the correct elapsed time regardless of how late the interval fires. If a tick is delayed by 5ms, the next tick reads 5ms ahead to compensate. No drift, no stale closure.

Ref vs State — the core principle

Ask yourself: "Does changing this value need to trigger a re-render?" For intervalId — no. For startTime — no. For savedElapsed — no. For displayMs — yes (this is what the user sees). So only displayMs goes in state. Everything else goes in refs.

ℹ Interview Tip

Say: "I use wall-clock anchoring — recording Date.now() when the timer starts and computing elapsed on each tick — rather than accumulating ticks. This gives accurate results even if the browser throttles setInterval under load or when the tab is in the background."

Pause and Resume Logic

const pause = () => {
  clearInterval(intervalRef.current);
  // savedRef.current already holds the elapsed — updated on every tick
  setRunning(false);
};

const resume = () => {
  // Shift the anchor back by what's already elapsed
  startRef.current = Date.now() - savedRef.current;
  intervalRef.current = setInterval(() => { ... }, 10);
  setRunning(true);
};

On pause, savedRef.current already holds the exact elapsed milliseconds (updated by every tick). On resume, we re-anchor: startRef.current = Date.now() - savedRef.current. The next tick will compute Date.now() - startRef.current which correctly continues from the saved point.

Memory Leak Prevention

useEffect(() => {
  return () => clearInterval(intervalRef.current); // cleanup on unmount
}, []);

Without this, navigating away from the component leaves a setInterval running in the background indefinitely. It will call setDisplay on an unmounted component, triggering React's "Can't perform a state update on an unmounted component" warning — and burning CPU.

Time Formatting

function fmt(ms) {
  const cs = Math.floor(ms / 10) % 100;   // centiseconds (0-99)
  const s  = Math.floor(ms / 1000) % 60;  // seconds (0-59)
  const m  = Math.floor(ms / 60000);      // minutes (unbounded)
  return `${pad(m)}:${pad(s)}.${pad(cs)}`;
}
const pad = n => String(n).padStart(2, '0');

Each unit is derived from raw ms using integer division and modulo. The modulo ensures seconds wrap at 60 (not counting up past 59). Minutes are unbounded — a stopwatch can run for hours.

⚠ Common Pitfall: clearInterval in the wrong place

Calling clearInterval only in the reset handler but not in useEffect cleanup means unmounting while the timer is running leaves an orphaned interval. Always clean up in both places.

Debugging extension: stale closure versus stopwatch drift

A simple counter can fail because its interval captures the first value:

// Broken counter: every callback captured seconds === 0.
setInterval(() => setSeconds(seconds + 1), 1000);

setSeconds(value => value + 1) fixes that stale closure, but it only counts delivered ticks. Browsers can delay background timers, so a real stopwatch should derive display time from a timestamp and accumulated elapsed duration.

const elapsed = accumulatedMs + (running ? performance.now() - startedAt.current : 0);
// Pause: accumulatedMs = elapsed; running = false.
// Resume: startedAt.current = performance.now(); running = true.

One effect owns one interval or animation loop and cleans it up on pause and unmount. Reset while running sets accumulated time to zero and restarts the timestamp; reset while paused sets accumulated time to zero without starting the clock.

Acceptance checklist

  1. Repeated Start creates one owner, not duplicate loops.
  2. Pause, wait two seconds, and confirm elapsed stays fixed; resume accumulates from that value.
  3. Reset while running returns to zero and continues correctly; reset while paused remains stopped.
  4. Unmount while running: cleanup prevents a later update.
  5. Background the tab briefly: explain why tick counting can differ from wall-clock elapsed time.

Reference: Performance.now() and React useEffect (accessed 2026-09-08).

Interview Criteria

Wall-clock anchoring

Uses Date.now() - startTime instead of accumulating ticks

Ref vs State discipline

Interval ID and timestamp in refs, only display in state

Pause/Resume correctness

Correctly offsets anchor on resume using savedRef

Memory leak prevention

useEffect cleanup clears interval on unmount

Accurate time format

MM:SS.cs derived correctly from raw milliseconds

Debug extension: separates stale-closure correctness from timer drift.
Debug extension: defines pause, resume, reset, and cleanup ownership.

Time Checkpoints

0–3 min

0–3 min: Explain stale closure problem and wall-clock approach

3–7 min

3–7 min: Set up refs for intervalId and startTime

7–12 min

7–12 min: Implement start/pause/reset with savedRef offset

12–16 min

12–16 min: Write fmt() helper for MM:SS.cs

16–20 min

16–20 min: Add useEffect cleanup to prevent memory leak

20–25 min

20–25 min: Test all flows: start→pause→resume→reset

Streak

0 days

Last active: Sign in to track

Progress

0%

0/0 solved

Submitted

0

Solutions pushed to review history.

More Machine Coding

View all Machine Coding →
Machine CodingMedium

React Tic-Tac-Toe Engine

Machine CodingMedium

Performant Image Slider

Machine CodingMedium

API Search and Render Image

Machine CodingEasy

Accessible React Tabs

Machine CodingMedium

Recursive File Explorer

Machine CodingEasy

Tooltip

Related hubs

  • All machine coding
  • Frontend machine coding guide
  • React machine coding questions
  • UI coding interview questions
  • React UI interview questions
  • Apple interview
  • Netflix interview
  • Uber interview

FAQ

What skills does a Stopwatch interview question test?

Timer accuracy with setInterval/requestAnimationFrame, cleanup on unmount, start/pause/reset state transitions, and formatting elapsed time.

Common pitfalls?

Leaking intervals, drifting time, and messy state that cannot distinguish idle vs running vs paused.

Related practice?

Traffic light, progress bar, and image slider.