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