Stale closures in React hooks happen when a function captures state from one render and keeps using it after the value changes. Every render creates its own snapshot of props and state, and the functions from that render see only that snapshot. The symptom is a component that keeps using an old value.
Why each render keeps its own values
State in React is a snapshot. When a component renders, React takes the current props and state and runs the component body with them. A function created inside that render closes over those exact values.
When the component re-renders with new values, the old function still exists and still points at the old values. JavaScript closures work this way by design, and React simply follows the same rule.
The word stale describes a value that was fresh at creation time but is now out of date. The closure itself is not broken; it is faithfully using the snapshot it was given.
A timer that reads stale state
This counter is supposed to increase every second. The interval callback reads count, but the Effect lists no dependency. Because the Effect never re-runs, the callback never gets a newer count.
import { useEffect, useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => setCount(count + 1), 1000);
return () => clearInterval(id);
}, []);
return <p>{count}</p>;
}On the first render count is zero, so the interval callback closes over zero. Every tick calls setCount with zero plus one, and the counter stays stuck at 1 no matter how long it runs.
Fix it with an updater function
The cleanest fix is to stop reading count. Pass an updater function instead, and React supplies the latest value on each tick.
setCount((c) => c + 1);Now the Effect reads no state, so it needs no dependency and the interval is created once. The updater receives the latest value directly, so no captured state is involved. The counter climbs by one every second.
Or add the missing dependency
Sometimes the value really should drive the work. Adding it to the dependency array tells React to re-run the Effect with the fresh value, so the closure is rebuilt with the new snapshot. React tears down the old Effect and runs a fresh one, and the tradeoff is that the work runs again.
How the dependency array works explains when that re-run is the behavior you want.
What to learn next
Stale closures are the reason an Effect can see an old value. When you want the latest value without re-running the Effect, React useEffectEvent is the purpose-built tool.
Rune AI
Key Insights
- Every render captures its own snapshot of props and state.
- A function from an old render keeps reading that old snapshot.
- Use updater functions to avoid reading stale state in Effects.
- Add the missing dependency when the work should be reactive.
Frequently Asked Questions
What is a stale closure in React?
Why does each render have its own values?
How do I fix a stale closure?
Conclusion
Stale closures are the natural result of state snapshots. Read fewer values inside Effects by using updater functions, and add dependencies when the work truly needs to re-run.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Headless UI Components Explained: Logic Without Locked Styling
Understand headless UI components and how libraries like Radix give you unstyled, accessible behavior that you style yourself.