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
countvariable (a constant for that specific “snapshot”). - The
useEffectran only on the first render, so itssetIntervalis holding onto thecountvariable 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
useRefto store the count. Since refs are objects with a mutable.currentproperty, the closure stays “fresh” because it's looking at the same object reference, not a snapshot of a primitive.