Stale Closures in React Hooks: Why They Happen

A stale closure captures state from an old render and keeps using it after the value changes. Learn why it happens and how to fix it.

6 min read

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.

App.jsxApp.jsx
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.

App.jsxApp.jsx
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

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.
RunePowered by Rune AI

Frequently Asked Questions

What is a stale closure in React?

A function that captured a value from an earlier render and keeps using that value after the state has changed.

Why does each render have its own values?

React treats state as a snapshot. A component body runs with the current props and state, and functions created there close over that snapshot.

How do I fix a stale closure?

Pass an updater function so the Effect does not read the old state, or add the value to the dependency array so React re-runs the Effect.

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.