Why Calling setState Twice Only Updates Once
Calling setState twice in the same handler often only applies one update. The fix is the functional update pattern, and understanding it requires understanding React state batching.
I had a step counter. Clicking Next should have moved forward two steps. It only moved one.
The code looked fine. I called the setter twice. No errors. The counter just ignored one of the calls.
This is the React state batching problem, and once you see it, you will never forget it.
The Broken Code
Here is the setup: a step state variable, and an event handler that calls the setter twice.
const [step, setStep] = useState(1);
function handleNext() {
setStep(step + 1); // should go to 2
setStep(step + 1); // should go to 3
}Click the button. Step goes to 2. Not 3.
Both calls ran. Both calls were to step + 1. But step was 1 in both of them, so both calls set the state to 2.
The second call did not build on the result of the first. Both read from the same snapshot.
What React State Batching Actually Is
When an event handler runs, React does not re-render after every setState call. It collects all the state updates from the entire handler and applies them in one batch at the end.
This is by design. It prevents cascading re-renders. If you update three pieces of state in one click handler, React re-renders once, not three times.
But there is a consequence: every setState(value) call in the same event handler reads the same snapshot of state. The state from the beginning of the event. Not the "latest" state after the previous setState call.
// All three reads of `step` here are the same value
function handleClick() {
setStep(step + 1); // step is 1, sets to 2
setStep(step + 1); // step is still 1, sets to 2 again
setStep(step + 1); // step is still 1, sets to 2 again
}
// Net result: step becomes 2, not 4The variable step in the component does not update mid-handler. It is frozen at the value it had when the render happened.
The Fix: Functional Updates
Instead of passing a value, pass a function. React will call that function with the most recently queued state value, not the stale snapshot.
function handleNext() {
setStep(s => s + 1); // receives 1, returns 2
setStep(s => s + 1); // receives 2 (the queued value), returns 3
}React processes these as a queue. The first call enqueues: "take current state, add 1." The second call enqueues: "take whatever the queue produces, add 1." The final result is 3.
ExpandTwo timelines side by side. Left shows two setStep(step+1) calls both reading the same stale value of 1, both enqueuing 2, net result 2. Right shows two setStep(s => s+1) calls chaining through the queue, first producing 2 then 3, net result 3.
The Rule
Use the functional form whenever the new state depends on the current state.
// Value form: safe when the new state is independent
setIsOpen(false);
setName('Denver');
// Functional form: required when new state derives from current state
setCount(c => c + 1);
setStep(s => s - 1);
setIsOpen(prev => !prev);The value form is fine when you are setting a fixed value that has nothing to do with what the state was before. The functional form is required when you are computing the next state from the current one.
Even if you only call the setter once, the functional form is still safer in concurrent mode, where React might call your component multiple times before committing. The callback always gets the most recent queued value. A plain value does not.
In Practice
This shows up most often in three patterns:
Toggling a boolean:
// Fragile in concurrent mode or double-calls
setIsOpen(!isOpen);
// Correct
setIsOpen(prev => !prev);Incrementing a counter:
// Fine if called once per event, fragile otherwise
setCount(count + 1);
// Always correct
setCount(c => c + 1);Building on previous array or object state:
// Correct
setItems(prev => [...prev, newItem]);React only re-renders once after the entire event handler. All the functional updates are queued and processed in order before that single re-render. The result is always consistent.
The Essentials
- React batches all
setStatecalls in one event handler into a single re-render. EverysetState(value)in that handler reads the same stale snapshot of state from the beginning of the event. - The functional update form
setState(prev => newValue)receives the most recently queued state value, not the stale snapshot. This is the correct pattern whenever new state depends on current state. - Use the value form for independent updates, the functional form for derived updates. If you ever need to call the same setter twice in one handler, you need the functional form.
Further Reading and Watching
- Queueing a Series of State Updates -- React Docs: The official explanation of how React queues state updates and why the functional form produces different results.
- useState -- React Docs: The full useState reference with sections on updater functions and their guarantees.
Keep reading