Why does setCount(count + 1) inside useCallback([], …) capture stale state
`useCallback(() => setCount(count + 1), [])` captures `count` from the render the callback was created (closure). Empty deps → it never updates → always uses the initial `count`. Fix: include `count` in deps (callback identity changes), or use the functional setter `setCount(c => c + 1)` (doesn't read outer `count`), or a ref for 'latest'.
Classic stale-closure bug. The cause is straightforward once you see it; the cure has three idiomatic forms.
The bug
function Counter() {
const [count, setCount] = useState(0);
const inc = useCallback(() => {
setCount(count + 1); // 'count' captured here
}, []); // empty deps → callback never updates
return <button onClick={inc}>{count}</button>;
}Click once → count becomes 1. Click again → still becomes 1 (not 2). Forever.
Why
useCallback(fn, []) returns the same fn reference on every render. That fn closes over the variables from the render where it was created. On the first render, count was 0. The closure captured 0. Every subsequent invocation reads count as 0.
setCount(0 + 1) = always sets to 1.
Fix 1 — include count in deps
const inc = useCallback(() => {
setCount(count + 1);
}, [count]); // re-create when count changesNow useCallback returns a new fn each time count changes. The closure captures the latest count.
Cost: the callback identity changes on every count change — any memoized child consuming it will re-render. Defeats the purpose of useCallback in this case.
Fix 2 — functional setter (preferred)
const inc = useCallback(() => {
setCount((c) => c + 1); // doesn't read outer 'count'
}, []);The functional setter receives the current value from React's state — no closure on count needed. The deps can stay empty; identity is stable.
This is the right answer for "increment by 1" cases.
Fix 3 — ref for "latest value"
const countRef = useRef(count);
useEffect(() => { countRef.current = count; });
const inc = useCallback(() => {
setCount(countRef.current + 1);
}, []);Refs always hold the latest value. Use when you need to read multiple state values inside a stable callback and a functional setter isn't enough.
Why this matters beyond counters
Stale closures bite hardest when:
- The callback is passed to a memoized child — and you want stable identity.
- The callback is in an effect with empty deps (
useEffect(() => { /* uses state */ }, [])). - The callback runs asynchronously (timer, fetch resolution) and reads state captured at start.
Same fix patterns apply.
The ESLint rule
react-hooks/exhaustive-deps catches "stale closure suspect" by warning about missing deps. Don't suppress it without understanding why — it's right almost always.
The mental model
A function captures the values in its enclosing scope at the moment it's created.useCallback'sdepsarray controls when a new function is created. If your function reads a value that changes, either include the value in deps, or rewrite the function so it doesn't read the outer value (functional setter, ref).
Interview framing
"The callback is wrapped in useCallback with empty deps, so React returns the same function reference forever. That function was created on the first render; it closed over count = 0 from that render. On every click it computes 0 + 1 and sets count to 1 — never 2. The fix depends on the use case: if it's literally 'increment by 1', use the functional setter setCount(c => c + 1) — no closure needed and identity stays stable. If the callback needs the latest count for other reasons, either include count in deps (loses memoization) or use a ref that always tracks latest. The deeper rule: a function captures values at creation; useCallback's deps array decides when to recreate. Stale closures are this rule biting you."
Follow-up questions
- •Why doesn't the closure see the latest count?
- •What does the functional setter do differently?
- •When is the ref pattern the right fix?
- •Why is `exhaustive-deps` lint useful here?
Common mistakes
- •Empty deps + outer state read.
- •Suppressing exhaustive-deps without understanding.
- •Reaching for ref when functional setter would work.
- •Adding state to deps and re-creating the callback unnecessarily.
Performance considerations
- •Functional setter keeps callback identity stable → memoized children don't re-render. The ref pattern is even more stable but harder to reason about.
Edge cases
- •Effect with empty deps and a captured state read.
- •setTimeout callback closing over state.
- •Custom hook returning a stable callback that reads state.
Real-world examples
- •Long-lived timers/animations.
- •Async submit handlers that should reflect the latest form state.