Back

Stopwatch

easy

Streak

0 days

Progress

0%

Submitted

0

Stopwatch

React30 mineasyFreeNew

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

Empty data and first-load state
Slow network, failed request, and retry path
Keyboard navigation and focus movement
Large input size, re-render pressure, and cleanup

Common mistakes

  • Starting with JSX before naming state and events.
  • Ignoring accessibility until the final minute.
  • Over-building abstractions instead of finishing the required behavior.
  • Failing to narrate trade-offs while coding.
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.

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

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.