← Back to debugging

The “Stale Closure” Interview Example

In a React-style functional component, the code looks like this. This is the one that trips up most candidates:

1function CounterComponent() {
2  const [count, setCount] = useState(0);
3
4  useEffect(() => {
5    const timer = setInterval(() => {
6      // THE TRAP:
7      // This function was created during the FIRST render.
8      // At that time, 'count' was 0.
9      console.log("Count is:", count); 
10    }, 1000);
11
12    return () => clearInterval(timer);
13  }, []); // Empty dependency array means this effect only runs once
14
15  return <button onClick={() => setCount(count + 1)}>Increment</button>;
16}

Why it “freezes”

  • React components are functions that re-run on every render.
  • Each render has its own count variable (a constant for that specific “snapshot”).
  • The useEffect ran only on the first render, so its setInterval is holding onto the count variable from Render #1 (which is 0).
  • Even if the component re-renders and a new count (e.g., 1) exists in Render #2, the interval is still executing the function from Render #1.

How to fix it (the “senior” answer)

In an interview, you should offer these solutions:

  • Add the dependency: Put [count] in the dependency array. This kills the old timer and starts a new one with the fresh value. (Cons: the timer resets every click.)
  • Functional updates (the pro fix): Use the functional version of state updates if you just need the previous value:
    1setCount(prevCount => prevCount + 1);
  • Refs: Use useRef to store the count. Since refs are objects with a mutable .current property, the closure stays “fresh” because it's looking at the same object reference, not a snapshot of a primitive.