Back to React
React
medium
mid

Why do functional state updates matter inside useCallback with empty dependencies?

With empty deps, the callback closes over the state value from its first render — forever stale. setCount(c => c + 1) reads the latest state from React's updater queue instead of the captured variable, so the callback stays correct without listing state as a dependency.

5 min read·~8 min to think through

This is a closure-staleness problem, and functional updates are the clean fix.

The problem: stale closure

jsx
const [count, setCount] = useState(0);

const increment = useCallback(() => {
  setCount(count + 1);   // ⚠️ 'count' is captured from THIS render
}, []);                  // empty deps → callback created once, never recreated

useCallback with [] deps creates the function once and never recreates it. That function closes over count from the first render — where count is 0. Forever.

So no matter how many times you call increment:

  • It always runs setCount(0 + 1) → sets count to 1.
  • The closed-over count never updates, because the callback is never recreated.
  • Click 5 times → count is 1, not 5. Stale closure bug.

The fix: functional update

jsx
const increment = useCallback(() => {
  setCount(prev => prev + 1);  // ✅ 'prev' = the latest state from React
}, []);

The functional form setCount(prev => ...) doesn't read the captured count variable at all. React calls your updater with the most recent state value from its internal update queue. So the callback is correct regardless of which render's closure it came from — and the empty deps array is now genuinely safe.

Why this matters specifically with useCallback([])

Two options to keep a callback correct:

  1. Add count to the deps → the callback is recreated every time count changes (new function identity each time) — defeats part of the point of memoizing it, and can cascade re-renders/effects in children that depend on its identity.
  2. Use a functional update + empty deps → the callback has a stable identity for the whole lifetime of the component and is always correct.

Option 2 is usually what you want — a stable, correct callback. That stable identity is exactly why you reached for useCallback (e.g. passing it to a memoized child).

The general principle

When the next state depends on the previous state, use the functional updater. It decouples correctness from what the closure captured — relevant in useCallback, useEffect with empty deps, event handlers set up once, setInterval callbacks, etc. It also correctly handles batched/multiple updates in one tick (setCount(c=>c+1); setCount(c=>c+1) → +2; the direct form would only +1).

How to answer

"With empty deps, useCallback builds the function once, closing over state from the first render — that value goes stale and never updates, so setCount(count + 1) keeps setting it to 1. setCount(prev => prev + 1) ignores the captured variable and gets the latest state from React's queue, so the callback stays correct and keeps its stable identity. The rule: when next state depends on previous state, use the functional updater."

Follow-up questions

  • What's the alternative to functional updates here, and why is it worse?
  • Why does a stable callback identity matter (e.g. with React.memo)?
  • Where else do stale closures bite — useEffect, setInterval?
  • How do functional updates handle multiple setState calls in one tick?

Common mistakes

  • Using setCount(count + 1) inside a callback/effect with empty deps — stale value.
  • Adding state to deps just to fix staleness, losing the stable identity.
  • Not recognizing the same bug in setInterval / event listeners set up once.
  • Assuming the closure 'sees' the latest state — it sees its render's snapshot.

Performance considerations

  • Functional updates let you keep empty deps, so the callback identity is stable for the component's life — important when passing it to memoized children (no needless re-renders) or as an effect dependency.

Edge cases

  • Multiple setState calls in one event handler (functional form composes correctly).
  • setInterval created once needing the latest state.
  • useEffect with [] deps referencing state.
  • Derived state depending on multiple pieces of state.

Real-world examples

  • An increment/toggle handler passed to a memoized child.
  • A setInterval tick that must read the latest count without resetting the interval.

Senior engineer discussion

Seniors explain it as closure capture: empty-deps useCallback freezes the function (and the state it closed over) at first render; the functional updater sidesteps the closure entirely by reading from React's update queue. They articulate the tradeoff vs adding deps (stable identity vs recreation) and generalize the rule to any next-depends-on-previous update.

Related questions