The Problem

A stale closure happens when a function captures a state value from a previous render and never sees the updated value. This is extremely common with setInterval, setTimeout, and event listeners.

Symptoms

  • A counter stops incrementing after the first update
  • Form inputs show old values in submit handlers
  • setInterval callback always logs the same value
  • Event handlers read state from when they were created, not current state

Real Error Scenario

```javascript // BAD: staleValue captures the initial value and never updates function Timer() { const [seconds, setSeconds] = useState(0);

useEffect(() => { const id = setInterval(() => { console.log('seconds:', seconds); // Always logs 0! setSeconds(seconds + 1); // Always sets 1! }, 1000); return () => clearInterval(id); }, []); // Empty deps = closure captures initial seconds=0

return <div>Elapsed: {seconds}s</div>; } ```

The counter displays "Elapsed: 1s" and never increments further. The interval callback captured seconds = 0 from the first render and never sees updates.

How to Fix It

Fix 1: Functional State Update (Simplest)

```javascript function Timer() { const [seconds, setSeconds] = useState(0);

useEffect(() => { const id = setInterval(() => { setSeconds(prev => prev + 1); // Uses latest value }, 1000); return () => clearInterval(id); }, []);

return <div>Elapsed: {seconds}s</div>; } ```

The functional updater prev => prev + 1 always receives the current state value, regardless of closure capture.

Fix 2: Include State in Dependency Array

```javascript function Timer() { const [seconds, setSeconds] = useState(0);

useEffect(() => { const id = setInterval(() => { setSeconds(seconds + 1); }, 1000); return () => clearInterval(id); }, [seconds]); // Re-create interval when seconds changes

return <div>Elapsed: {seconds}s</div>; } ```

This works but re-creates the interval every second, which is wasteful.

Fix 3: useRef for Latest Value

```javascript function Timer() { const [seconds, setSeconds] = useState(0); const secondsRef = useRef(seconds);

useEffect(() => { secondsRef.current = seconds; }, [seconds]);

useEffect(() => { const id = setInterval(() => { console.log('Current seconds:', secondsRef.current); setSeconds(secondsRef.current + 1); }, 1000); return () => clearInterval(id); }, []);

return <div>Elapsed: {seconds}s</div>; } ```

useRef gives you a mutable reference that the closure always reads the latest value from.

When to Use Each Fix

  • Use functional updates when new state depends only on previous state
  • Use useRef when you need to read the latest value inside a stable callback
  • Use dependency array when the effect truly needs to re-run on state change