All topics
Hooksintermediate

Stale Closures in Hooks

Understand why hooks can capture outdated values in their closures and the common ways to avoid or intentionally leverage this.

A 'stale closure' happens when a function created during one render — like an effect callback, an event handler, or a setTimeout callback — captures a prop or state value from that specific render, and that captured value becomes outdated ('stale') by the time the function actually executes, because a newer render has since produced a fresher value the closure doesn't see.

A stale closure is like a photograph of the whiteboard taken at the start of a meeting — if you refer back to that photo later assuming it reflects everything currently written on the board, you'll miss every edit that happened since the picture was snapped.

Key Concepts

1
This is a natural consequence of how JavaScript closures work combined with how React re-creates functions on every render: each render's version of a function closes over that render's specific variables. If an effect has an incomplete dependency array, or a setTimeout scheduled from an old render fires after several re-renders, the code inside still sees the props/state as they were at creation time, not the latest values.
setTimeout
2
The most common fixes are: including all the values actually used inside the function in the relevant dependency array (letting the linter's exhaustive-deps rule guide this), using the functional updater form of setState (setCount(c => c + 1)) to always operate on the latest state without needing it as a dependency, or storing the latest value in a ref that's always read fresh regardless of which render's closure is executing.
exhaustive-depssetStatesetCount(c => c + 1)
3
Interviewers frequently present a broken counter or timer example (setInterval incrementing by referencing stale count from the render where the interval was created) and ask candidates to both diagnose why it's stale and propose at least two different valid fixes.
setIntervalcount