Skip to main content

Queueing a Series of State Updates

The last page showed that state is a snapshot, so calling a setter three times with the same value only bumps it once. This page is the other half: what if I genuinely want several updates to stack in one handler? The answer is React's update queue and the updater function.

React batches state updates

Setting state doesn't re-render on the spot. React waits until the whole event handler finishes, then processes all the queued updates together and re-renders once. This is called batching.

plus-three.jsx
export default function Counter() {
const [number, setNumber] = useState(0);

return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}>+3</button>
</>
);
}

This still only adds 1. As we saw, each render's state values are fixed, so number is 0 throughout this handler and all three calls are really setNumber(0 + 1). React batches them, and since they all say "make it 1," the next render is 1.

Batching is a good thing. It avoids re-rendering after every single setter, it prevents "half-finished" renders where some state updated and some didn't, and it makes the app faster. One thing to note: React batches within a single event, but it does not batch across separate events. Two clicks are handled separately, each with its own render.

Updating the same state multiple times before the next render

To make several updates stack, pass a function to the setter instead of a value. This is an updater function.

plus-three-fixed.jsx
<button onClick={() => {
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);
}}>+3</button>

Now it adds 3. The difference is what React does with what you pass. A plain value means "replace the state with this." A function means "take whatever is queued so far and compute the next value from it." React queues each updater and, on the next render, runs them in order, feeding each one the result of the previous.

Reading n => n + 1 starting from number = 0:

queued updatenreturns
n => n + 101
n => n + 112
n => n + 123

Final value: 3. The key distinction to hold onto is that setNumber(number + 1) uses the fixed snapshot value, while setNumber(n => n + 1) uses the latest queued result, which is why the updater form stacks and the value form doesn't.

What happens if you update state after replacing it

Mix the two and it still follows one simple rule: React walks the queue top to bottom, a value "replaces," a function "computes from the previous."

replace-then-update.jsx
<button onClick={() => {
setNumber(number + 5); // number is 0, so: replace with 5
setNumber(n => n + 1); // updater: take 5, return 6
}}>
queued updatenreturns
"replace with 5"0 (ignored)5
n => n + 156

Final value: 6.

What happens if you replace state after updating it

Add a plain value at the end and it throws away whatever was computed before it, because a value means "replace," ignoring the queue so far.

update-then-replace.jsx
<button onClick={() => {
setNumber(number + 5); // replace with 5
setNumber(n => n + 1); // take 5, return 6
setNumber(42); // replace with 42
}}>
queued updatenreturns
"replace with 5"0 (ignored)5
n => n + 156
"replace with 42"6 (ignored)42

Final value: 42. The last plain value wins because it replaces everything queued before it.

So the whole rule in one line: an updater function is added to the queue and builds on the previous result, while any other value is added as "replace with this" and discards what came before it. And updater functions must be pure: just compute and return the next state, no side effects, no calling setters inside them.

Note. Passing a plain value like setNumber(5) is really just setNumber(n => 5) where n is ignored. That's why a value always "replaces" and pays no attention to the queue.

Naming conventions

The updater's argument is usually named after the state. A common short style is the first letters of the state variable:

naming.jsx
setEnabled(e => !e);
setLastName(ln => ln.reverse());
setFriendCount(fc => fc * 2);

If you prefer clarity over brevity, spell it out with the full name or a prev prefix, both are fine:

naming-verbose.jsx
setEnabled(enabled => !enabled);
setEnabled(prevEnabled => !prevEnabled);

Recap

Setting state doesn't change the variable in the current render, it requests a new one. React processes all the state updates from an event handler after the handler finishes, which is batching, and that means one re-render for many setter calls. When you need to update the same state several times in one event (or base it reliably on the previous value), use the updater form setNumber(n => n + 1) so each update builds on the last instead of overwriting it.