Stale Closures in Hooks

A stale closure occurs when a hook callback retains values from an older render instead of seeing the latest props or state. It commonly appears when dependency arrays are incomplete or when asynchronous callbacks outlive the render that created them.

In React, each render creates its own bindings for props and state. A function created during that render closes over those bindings, so later renders do not change what that particular function already captured.

This becomes visible with `useEffect`, timers, subscriptions, promises, event listeners, and memoized callbacks. If an effect has an empty dependency array, for example, its callback is normally created from the initial render and will not be recreated when referenced state changes.

The usual fix is to declare every reactive value used by an effect or memoized callback in its dependency list. React can then recreate the callback or rerun the effect when those values change. For state updates that depend only on the previous state, a functional updater such as `setCount(c => c + 1)` avoids depending on a captured `count` value.

A `useRef` can provide another solution when a long-lived callback needs access to the latest mutable value without causing rerenders. Update `ref.current` during rendering or through an appropriate effect, then have the stable callback read `ref.current` when it runs.

The key distinction is between a function being stable and the values it reads being current. `useCallback` can preserve function identity, but it does not magically refresh captured values; its dependency list determines when a new closure is produced.

Key points

Interview questions

What is a stale closure in React?

A stale closure is a callback that continues to use props or state from an older render. React renders a component again when its state or props change, creating new bindings and potentially new functions. An existing callback does not automatically switch to the bindings from the newest render. This is why timers, effects, and listeners can observe old values.

How do dependency arrays help prevent stale closures?

A dependency array tells React which reactive values an effect or memoized callback uses and when the associated work should be recreated or rerun. If a changing value is referenced but omitted, React may retain a callback that captured an older value. Including the value makes changes to it observable to the hook. Dependencies should therefore reflect the reactive values actually used by the callback.

Why does `setCount(count + 1)` sometimes produce a stale-state bug, and how does the functional updater fix it?

`count + 1` uses the `count` value captured by the callback's render. If several updates are queued from callbacks that captured the same value, they can all calculate the same next state. The functional form, `setCount(c => c + 1)`, asks React to apply the calculation to the state value available when the update is processed. This removes the callback's dependency on its captured `count` value for that calculation.

How would you keep a long-lived event listener reading the latest value?

One option is to recreate the listener whenever the relevant value changes by declaring that value as an effect dependency. Another is to keep the latest value in a ref and have a stable listener read `ref.current`. The choice depends on whether changing the value should cause the subscription itself to be recreated. A ref is useful when the external subscription should remain stable while the data it reads changes.

Does `useCallback` prevent stale closures?

No. `useCallback` memoizes a function identity until one of its dependencies changes, but the function still closes over the values from the render that created it. If a dependency is missing, the memoized function can retain an outdated value. Correct dependencies are therefore still essential. If the callback does not need a changing value directly, a functional updater or ref may remove that dependency for a specific use case.