How to Fix a React useState Stale-State Bug
Calling setCount(count + 1) three times in one React handler adds 1, not 3 - each call closes over the same stale count from the current render. Switch to the functional updater form and each call gets the latest pending state instead.
What the stale-state bug looks like
You have a handler that fires setState multiple times:
function addThree() {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
}
Click the button when count is 0. You expect 3, but the counter shows 1.
All three calls read count from the enclosing render scope - it's 0 for all
of them. React batches the three identical 0 + 1 = 1 updates into a single
state transition, so only one increment lands.
The fix: functional updater form
Pass a function to setState instead of a value. React calls that function with
the latest pending state as the argument, so each call sees the result of the
previous one:
function addThree() {
setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1);
}
Now the sequence is:
- First call: c = 0 -> 1
- Second call: c = 1 -> 2
- Third call: c = 2 -> 3
React applies the updates in order, and the counter correctly reaches 3.
Why the value form usually seems fine
A single setCount(count + 1) per click works correctly because React
re-renders between clicks - so count is fresh the next time the handler runs.
The stale-closure bug only bites when you queue multiple updates in one
handler, in a loop, or inside an async callback where the render-time
snapshot is already out of date.
When to always use the functional form
Use setState(prev => ...) whenever:
- You call
setStatemore than once inside a single function - You set state inside a
setTimeout,setInterval, orasynccallback - You derive the new value from the old value (counters, toggles, list appends)
A simple rule: if the new value depends on the previous value, use the
functional updater. If it's completely independent (e.g. setName("Alice")),
either form is fine.
Catching it early with ESLint
The react-hooks/exhaustive-deps rule flags stale dependencies in useEffect
hooks. Enable it in your project's ESLint config:
{
"rules": {
"react-hooks/exhaustive-deps": "warn"
}
}
For multi-setState handlers, code review and test coverage (a "+3 button should add 3" assertion) catch it before it ships.
Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.
Try it in a workspace →What you'll practice
- Spotting stale-closure bugs where multiple setState calls compute the same value
- Switching from the value form (setCount(count + 1)) to the functional updater form (setCount(c => c + 1))
- Identifying the scenarios where the functional updater is necessary: loops, async callbacks, batched updates
FAQ
Why does calling setCount multiple times in React only update once?
Each call captures the same count value from the current render. React batches the identical updates into one state transition. Use the functional updater form setCount(c => c + 1) so each call receives the latest pending state.
What is the functional updater form in React useState?
Instead of setState(value), you write setState(prev => newValue). React passes the latest pending state as prev, so multiple calls in one handler chain correctly rather than all reading the same stale value.
When should I use the functional form of setState?
Whenever the new state depends on the previous state - counters, toggles, appending to a list, or any time you call setState more than once in one handler or inside an async callback.
Keep learning
Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.
Open the workspace →