Topic 4.5
Batching and Updater Functions
In one line
React collects all the state updates from one event into a queue and renders once at the end (batching); pass an updater function like setN(n => n + 1) when the next value depends on the previous one, so each update builds on the latest queued value.
Think of it like this
A waiter taking a table's order. The waiter doesn't run to the kitchen after each person speaks. They listen to everyone, write the whole order down, and only then go to the kitchen once. React does the same with state updates: it writes every update from one event into a queue and renders once when the event handler has finished. This is called batching.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Batching
- React grouping several state updates into one re-render.
- Automatic batching
- Since React 18, updates are batched everywhere: event handlers, timers, promises.
- Update queue
- React's list of state updates waiting to be applied in the next render.
- Updater function
- A function you pass to a setter, like
n => n + 1, that computes the new value from the previous one. - Pending state
- The value after all earlier updates in the queue have been applied: what an updater receives.
- Replace
- Passing a plain value to a setter: it throws away the previous value and uses this one.
- flushSync
- A
react-domfunction that forces React to apply updates right now instead of batching them.
Step by step
01Three setters with a value
The snapshot says number is 0 in all three lines, so each line queues the same thing.
// render where number is 0
setNumber(number + 1); // setNumber(0 + 1): replace with 1
setNumber(number + 1); // replace with 1
setNumber(number + 1); // replace with 1
// next render: number is 102Three setters with an updater
Each updater is queued. In the next render React runs them in order, passing each the result of the previous one.
setNumber((n) => n + 1);
setNumber((n) => n + 1);
setNumber((n) => n + 1);03Work through a mixed queue
Go through the queue in order and write down what each update produces. Plain values ignore what came before.
1. replace with 5 gives 5. 2. n => n + 1 receives 5 and gives 6. 3. replace with 42 ignores the 6 and gives 42. The next render shows 42.
// number is 0
setNumber(number + 5);
setNumber((n) => n + 1);
setNumber(42);
// next render: 4204One render for the whole event
However many setters run in one handler (or one timer callback), React renders once at the end. The screen never shows a half-applied update.
05Pick sensible updater names
Name the argument so a reader knows which state it is.
setEnabled((e) => !e);
setLastName((ln) => ln.trim());
setFriendCount((fc) => fc * 2);
setCount((prevCount) => prevCount + 1);Try it yourself
- 1
Predict, then check
In the quiz, add a Case C:
setNumber((n) => n + 1); setNumber(number + 5);. From 0, predict the result before running. (5: the replacement comes last and uses the snapshot.) - 2
Count the renders
In the first example, make the plain button call
setPlainten times. How manyrender:lines appear for one click? (Still one.) - 3
Make the updater impure
Wrap the first example's render in
<StrictMode>and addconsole.log("updater ran")inside an updater. Each updater runs twice in development, which is why updaters must only calculate.
Code & diagrams
Each click causes exactly one render (batching), even with three setter calls. Values add 1; updaters add 3.
Preview
Console
Nothing logged yet.Expected console output
render: plain = 0 | updater = 0
render: plain = 1 | updater = 0
render: plain = 1 | updater = 3Before React 18 (with the old `ReactDOM.render`), the three updates after `await` would have caused three renders. With `createRoot` they are batched into one.
Preview
Console
Nothing logged yet.Expected console output
render: guest | idle | 0
render: Asha | saved | 1Case A: replace with 0 + 5, then 5 + 1 = 6. Case B starts from 6: replace with 11, then 12, then replace with 42. A plain value wipes out everything queued before it.
Preview
Console
Nothing logged yet.Expected console output
after case A: 6
after case B: 42Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
A +3 button that only adds 1
Write a score button whose handler calls setScore(score + 1) three times.
Break #2
Rely on a render between two setters
Call setItems(newItems) and then, in the same handler, read items.length to update a counter: setCount(items.length).
Myth vs fact
Myth
Each setter call causes its own render.
Fact
Since React 18, all updates in one event (or one timer, promise or native callback) are batched into one render.
Myth
Updater functions are only for counters.
Fact
Any state calculated from its previous value benefits: toggling (setOpen((o) => !o)), appending to arrays, merging objects.
Myth
You should always use updaters and never plain values.
Fact
Plain values are fine when the new state doesn't depend on the old one (setName("Asha")) or when you update only once per event. Updaters matter when updates stack up.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Opt out of batching with
flushSync(() => setX(1))fromreact-dom: React renders and commits synchronously beforeflushSyncreturns. It is slow and rarely needed (measuring or scrolling to a newly added DOM node, Phase 8). - ▸
Updates are processed by priority lanes: a click's update is urgent; updates inside
startTransitionare lower priority and can be interrupted (Topic 16.7). Updaters in a low-priority lane are replayed on top of urgent ones, so the final state is the same as if they'd run in order. - ▸
In StrictMode, React calls updater functions twice (and keeps one result) to expose impure updaters, such as ones that push into an array they were given.
- ▸
Automatic batching only applies to roots created with
createRoot. The oldReactDOM.render(removed in React 19) kept React 17's behaviour.
Remember this
- 1
Batching means grouping several state updates into a single re-render. It makes apps faster (one render instead of five) and avoids showing a half-updated screen where one value changed but another hasn't yet. React only starts the render after all the code in your handler has run.
- 2
Automatic batching (React 18+). Before React 18, only updates inside React event handlers were batched; updates inside
setTimeout, promises or native listeners each caused their own render. WithcreateRoot(React 18 and later), every update is batched, wherever it comes from. React never batches across separate intentional events: two clicks are two renders, so a form can't be submitted twice by one batch. - 3
The classic surprise:
setN(n + 1); setN(n + 1); setN(n + 1);adds 1, not 3. Because of the snapshot (Topic 4.4),nis0in all three lines, so you queue "replace with 1" three times. The queue ends at 1. - 4
Updater functions. Instead of a value, pass a function:
setN((n) => n + 1). React puts the function in the queue and, during the next render, runs each one in order, feeding each the result of the previous one:0 → 1 → 2 → 3. The function receives the pending state (the latest value after all earlier updates in the queue), not the snapshot. An updater must be pure: only calculate and return the new value, no side effects, because StrictMode calls it twice to check. - 5
Mixing values and updaters. The queue is processed in order: a plain value means "replace with this", an updater means "compute from the previous".
setN(n + 5); setN((x) => x + 1);withn = 0gives6. AddingsetN(42)at the end gives42, because a replacement throws away whatever came before. - 6
When to use an updater: whenever the new state is calculated from the old one and you might update more than once before the next render, or the callback may run with a stale snapshot (timers, intervals, async code). Many teams use updaters for every "based on previous" update for consistency; it's never wrong. Naming: call the argument after the first letters of the state (
setEnabled((e) => !e),setLastName((ln) => ln.toUpperCase())) or repeat the full name (setCount((prevCount) => prevCount + 1)), so it's clear what it holds.
Explain it without notes
What is batching, and what changed in React 18?
Why does calling setN(n + 1) three times add only 1?
How does an updater function work, and what does it receive?
With n = 0, what is the result of setN(n + 5); setN((x) => x + 1); setN(42); and why?
Practice
A Likes component has a +10 button that should call a helper addOne() ten times in a loop. Write addOne so the final count really goes up by 10.
Starting from n = 3, what does setN((x) => x * 2); setN(n + 1); setN((x) => x - 1); produce? Show the queue.
Trade-offs
- ↔
Updaters are always correct for derived updates but slightly noisier to read; plain values are clearer for simple replacements.
- ↔
Batching saves renders but means the DOM isn't updated until your handler ends;
flushSynctrades performance for that immediacy.
Done when you can
I can explain batching and automatic batching in React 18+.
I can predict the result of several setter calls in one handler.
I can use an updater function and know what it receives.
I can work through a queue that mixes values and updaters.
I name updater arguments clearly.