Why do functional updates (setCount(prev => prev + 1)) matter inside useCallback with empty deps
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.
This is a closure-staleness problem, and functional updates are the clean fix.
The problem: stale closure
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount(count + 1); // ⚠️ 'count' is captured from THIS render
}, []); // empty deps → callback created once, never recreateduseCallback 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 to1. - The closed-over
countnever updates, because the callback is never recreated. - Click 5 times → count is
1, not5. Stale closure bug.
The fix: functional update
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:
- Add
countto the deps → the callback is recreated every timecountchanges (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. - 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.