Explain how JavaScript closures capture values from a specific render, why React renders create new closures, and how this leads to stale state bugs in effects, timeouts, and event handlers — and how to fix them.
Why does React sometimes show stale state in closures? How do you fix it?
Use guided tracks for structured prep, then practice company-specific question sets when you want targeted interview coverage.
Short answer
Because each render creates new variables and new functions, and JavaScript closures capture the values from the render they were created in. If an async callback or effect runs later, it may still see old state from an earlier render.
The mental model
React components are not long-lived objects. They are more like:
"Functions that React calls again and again to produce UI."
Every render creates a new snapshot of props, state, and functions. Closures created in that render freeze those values.
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
console.log(count); // ❌ Might log stale value
}, 1000);
return () => clearInterval(id);
}, []); // empty deps
return <button onClick={() => setCount(c => c + 1)}>+</button>;
}
Why this logs stale state
This effect runs only once. The closure inside setInterval captures count from the first render (which is 0).
Even if state updates later, this callback still sees count = 0 forever. Not a React bug. Just JavaScript closures + React render model.
Cause | What’s actually happening | Symptom |
|---|---|---|
Closure captures old render values | Callback keeps references to old variables | State appears "stuck" or outdated |
Effect deps are missing | Effect never re-runs with new state | Logic uses old props/state |
Async logic (timeout, interval, promise) | Runs after render that created it | Reads stale snapshot |
Fix #1: Add correct dependencies
Tell React to recreate the closure when state changes:
useEffect(() => {
const id = setInterval(() => {
console.log(count);
}, 1000);
return () => clearInterval(id);
}, [count]); // ✅ now closure updates when count changes
Fix #2: Use functional updates (most common fix)
If you are updating state based on previous state, don’t read from the closure at all:
setCount(c => c + 1); // ✅ always uses latest state
Fix #3: Use useRef for mutable latest value
If you need to read the latest value from long-lived async callbacks:
const countRef = useRef(count);
useEffect(() => {
countRef.current = count;
}, [count]);
useEffect(() => {
const id = setInterval(() => {
console.log(countRef.current); // ✅ always latest
}, 1000);
return () => clearInterval(id);
}, []);
Important insight
This is not a React quirk. This is how JavaScript closures work combined with React’s "each render is a snapshot" model.
Interview framing
Say it like this:
"Each render creates a new closure with its own state snapshot. Async callbacks keep the snapshot from the render they were created in. Stale state happens when effects or callbacks don’t re-run with new dependencies. You fix it with correct deps, functional updates, or refs."
Stale state happens because closures capture values from a specific render. React re-renders create new snapshots, but old async callbacks keep old ones. Fix it by using correct dependencies, functional state updates, or a ref for reading the latest value.