Stale closures, and how to stop them
The short answer
A stale closure happens when a function captures state from the render in which it was created and keeps using that snapshot after the state has changed. It shows up most often in setInterval, setTimeout and event listeners registered once with an empty dependency array. The three fixes are: use the functional updater form of setState, add the value to the dependency array so the effect re-subscribes, or store the callback in a ref and always invoke the latest version.
It is the bug that makes people distrust hooks. The code looks right, React reports no error, and the number on screen simply refuses to move.
The classic reproduction
The effect runs once, on mount. At that moment count is 0, and the arrow function passed to setInterval closes over that binding. Every tick computes 0 + 1. The state does update to 1, which re-renders — but the interval is still holding the function from the very first render.
Key takeaway
Each render creates a new set of variables. A function created during a render sees that render's values permanently. Nothing "updates" a closure after the fact.
Fix 1: the functional updater
When the new state derives from the old, pass a function to the setter. React hands it the current value, so the closure never needs to know it.
count from the dependency array legitimately, so the interval is created once instead of being torn down and rebuilt every second.Fix 2: declare the dependency honestly
If the effect really does need the value — not just to update it — put it in the dependency array and let the effect re-run.
Fix 3: the latest-ref pattern
A ref is a mutable box shared by every render. Write the newest callback into it on each render, and have the long-lived subscription read from the box at call time.
The interval is created once and never restarted, yet each tick calls the newest callback with the newest state. This is what useInterval, useTimeout, useEventListener and useEvent in this library all do internally.
Where else it bites
Catching it before it ships
- Enable
react-hooks/exhaustive-depsand treat it as an error, not a warning. It catches the overwhelming majority of these. - When you deliberately omit a dependency, leave a comment explaining why — an unexplained disable is where the next bug hides.
- Prefer the functional updater form for any state derived from previous state, as a habit rather than a fix.
- Reach for a hook that already solves it.
useInterval,useTimeout,useEventListeneranduseEventexist precisely so this pattern is written once.
Hooks that handle this for you
Is a stale closure a React bug?
No, it is standard JavaScript closure behaviour. A function captures the variables in scope when it is created. React renders create a new scope each time, so a function that outlives its render keeps looking at the old one.
Why does adding the value to the dependency array work?
Because the effect then re-runs whenever the value changes, creating a new closure over the new value — and cleaning up the old subscription first. The trade-off is that the subscription is torn down and rebuilt each time.
When should I use a ref instead of a dependency?
When the subscription is expensive or must not be interrupted — an interval that needs a steady cadence, a WebSocket, an IntersectionObserver — but the callback still needs current state. That is exactly what the latest-ref pattern is for.
Does useEvent solve this everywhere?
It solves it for callbacks: you get a stable function identity that always reads current state. It does not help with values you read directly inside an effect body — for those you still need the dependency array or the updater form.
Keep reading
Writing custom hooks that survive production
Naming, return shapes, dependency arrays, cleanup, testing and TypeScript patterns for custom React hooks that other people have to maintain.
The React Hooks Cheat Sheet
A complete reference to every built-in React hook and the 44 custom hooks in @danixsoft/hooks — what each one does, when to reach for it, and the mistake people make with it.