Queueing a series of state updates
How React batches state updates into one render, and how updater functions like c => c + 1 build on the queued value. Live examples and a queue to step through.
- 01Explain batchingSay why React renders once after a handler that sets state several times, in clicks, timers, and after
await, and whatflushSyncchanges. - 02Use updater functionsWrite
setCount((c) => c + 1)when the next state depends on the queued one, and work out a queue of updates row by row. - 03Mix replacing and updatingPredict the result when a handler mixes
setPoints(50)withsetPoints((p) => p * 2), and name updater arguments clearly.
Add 3 teas that really adds 3
In State as a snapshot, a button called setCount(count + 1) three times and added only 1. Every call read the same count from the same render, so all three asked for the same number:
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);Here is the same button, with one change on lines 11 to 13: each call passes a function instead of a number. Click Add 3 teas in the result:
import { useState } from "react"; export default function App() { const [count, setCount] = useState(0); return ( <> <p>Items in cart: {count}</p> <button onClick={() => { setCount((c) => c + 1); setCount((c) => c + 1); setCount((c) => c + 1); }} > Add 3 teas </button> </> );}Now one click adds 3. (c) => c + 1 is an arrow function that takes a number c and returns one more. Passed to setCount, it's called an updater function. It doesn't say "make the count 1". It says "whatever the count is by then, add one".
Two things make this work. First, React doesn't render after each setCount. It puts each update in a queue, a list kept in order, and waits until your handler finishes. Second, when React works out the next state, it goes through the queue in order and hands each updater the result of the one before: 0 becomes 1, then 2, then 3.
count 3.A café server writes your order on a pad as you speak. They don't run to the kitchen after every word. You can say "a tea", then "one more tea", then "one more", and the pad says three teas. If you say "make it two teas", that line sets the number, whatever came before. The kitchen reads the whole pad, top to bottom, once you're done.
- In real life: The server writes each line on the pad while you talk
- In React: Each setCount call adds an update to the queue
- In real life: The kitchen starts only when you've finished ordering
- In React: React renders once, after the handler finishes
- In real life: "One more tea" builds on what's already on the pad
- In React: An updater, (c) => c + 1, builds on the queued value
- In real life: "Make it two teas" sets the number, whatever came before
- In React: A value, like setCount(2), replaces the queued value
React queues state updates. It waits until your event handler finishes, then works through every queued update, in order, and renders once. Pass a value, and React replaces the state with it. Pass an updater function, and React calls it with the queued value and uses what it returns.
React waits for your handler: batching
Most clicks change more than one piece of state. Render and commit showed that React turns several set calls into one render. Here is why that matters for a series of updates. Press Place order in this checkout:
import { useState } from "react"; export default function App() { const [items, setItems] = useState(2); const [status, setStatus] = useState("In your cart"); const [note, setNote] = useState(""); function handlePlaceOrder() { setItems(0); setStatus("Order placed"); setNote("We'll email your receipt."); } return ( <> <p>Items: {items}</p> <p>Status: {status}</p> <p>{note}</p> <button onClick={handlePlaceOrder}>Place order</button> </> );}Lines 4 to 6 keep three pieces of state: the number of items, a status, and a note. handlePlaceOrder sets all three on lines 9, 10, and 11. Each call adds an update to React's queue. Nothing renders until the handler ends; then React makes one render with all three changes, and the page changes all at once.
This grouping is batching. It has two benefits. React does less work: one render instead of three. And the page never shows a half-finished state, like Items: 0 next to Status: In your cart.
flushSync forces a render after each update, so the page shows two half-finished checkouts on the way.Since React 18, batching happens wherever the updates come from: event handlers, timers, promises, and the code after await (React 18 release notes). In React 17 and older, only updates inside React event handlers were batched; you may still see older code that works around that. If you ever need React to update the page before your handler goes on, flushSync from react-dom forces a render straight away. It's rarely needed, and it gives up batching.
Try every case in this playground. The checkout runs on this page with real React. Pick where its three updates run, press Place order, and count the renders. Then press Back to the cart and try the next one.
function handlePlaceOrder() {
setItems(0);
setStatus("Order placed");
setNote("We'll email your receipt.");
}0 rendersItems: 2
Status: In your cart
Note: (empty)
Renders for Place order
None yet.
Pick where the updates run, then press Place order in the checkout.
In the click, in a timer, and after an await, React makes 1 render with all three changes. The timer and the await wait a second first: nothing renders while they wait. With flushSync, React makes 3 renders, and the first two show a half-finished checkout: items gone, status still In your cart.
Each row is a place your updates can run, and what React does with three of them:
| Where the updates run | Batched in React 18 and later? | Renders for three updates |
|---|---|---|
| In a click handler | Yes. | One render, after the handler finishes. |
In a setTimeout callback | Yes. Before React 18, no. | One render, after the callback finishes. |
After await, or in a promise's then | Yes. Before React 18, no. | One render, after that code finishes. |
Each one inside its own flushSync | No. flushSync renders straight away. | Three renders, one per update. |
| Two separate clicks | No. Each click is its own event. | Two renders, one per click. |
The last row matters too. React never batches two separate clicks together: each click is its own event, and React renders for the first before it handles the second. So if the first click disables a button, the second click can't press it.
Updater functions work from the queued value
Here is what React does with setCount((c) => c + 1), step by step:
- While your handler runs, each call adds the function itself to the queue. (When nothing else is waiting, React may call the first one right away, as a shortcut to see whether anything changes. The result is the same. What setState really does shows that shortcut in React's code.)
- When React works out the next state, it starts from the state the last render had, here 0, and calls the first updater with it.
- It hands each updater's result to the next one, as its argument.
- The last result is the new state, and that's what
useStatereturns in the next render.
For Add 3 teas, starting from 0, each row is one queued update, the value it gets, and what it returns:
| Queued update | c it gets | What it returns |
|---|---|---|
c => c + 1 | 0 | 0 + 1 = 1 |
c => c + 1 | 1 | 1 + 1 = 2 |
c => c + 1 | 2 | 2 + 1 = 3 |
React keeps 3, and the next render shows Items in cart: 3. Build your own queues in this playground. The points counter starts at 10. Add calls to handleClick with the buttons, press Run handleClick on the page, then press Step to see how React got there, one row at a time.
function handleClick() {
setPoints(points + 5);
setPoints((p) => p * 2);
}click Run handleClickPoints: 10
How React worked through the queue
| Queued update | p it gets | It returns |
|---|---|---|
| Run the handler first. | ||
Add calls to handleClick with the buttons, then press Run handleClick on the page.
The playground starts with setPoints(points + 5) and setPoints((p) => p * 2). The page shows 30. Step through: the first row is "replace with 15" and the second, p => p * 2, gets 15 and returns 30. Run it again, and the queue starts from 30. The next section explains why the first row is a replacement.
Mixing replacing and updating
You can mix both kinds in one handler. Here a shop gives loyalty points: a welcome bonus of 5, then double points. Points start at 10. Predict the number, then press Bonus:
import { useState } from "react"; export default function App() { const [points, setPoints] = useState(10); function handleBonus() { setPoints(points + 5); setPoints((p) => p * 2); } return ( <> <p>Points: {points}</p> <button onClick={handleBonus}>Bonus</button> </> );}It shows 30. Line 7 passes points + 5, which is worked out right away from this render's points: 10 + 5. So React queues "replace with 15". Line 8 passes a function, so React queues p => p * 2 itself. Then React works through the queue. Each row is one queued update, the value it gets, and what it returns:
| Queued update | p it gets | What it returns |
|---|---|---|
| "replace with 15" | 10 (not used) | 15 |
p => p * 2 | 15 | 15 * 2 = 30 |
Now add one more line at the end, a fixed number:
import { useState } from "react"; export default function App() { const [points, setPoints] = useState(10); function handleBonus() { setPoints(points + 5); setPoints((p) => p * 2); setPoints(50); } return ( <> <p>Points: {points}</p> <button onClick={handleBonus}>Bonus</button> </> );}It shows 50. Line 9 queues "replace with 50", which ignores the 30 it gets. Press Bonus again: still 50, because the last update in the queue always wins. The same three columns, with the new row at the end:
| Queued update | p it gets | What it returns |
|---|---|---|
| "replace with 15" | 10 (not used) | 15 |
p => p * 2 | 15 | 15 * 2 = 30 |
| "replace with 50" | 30 (not used) | 50 |
(p) => p * 2 gets 15 and gives 30. "Replace with 50" ignores the 30 and gives 50, the points in the next render.So what you pass to a set function decides what goes in the queue:
- A function, like
(p) => p * 2, goes in the queue as it is. React calls it with the queued value. - Any other value, like
50orpoints + 5, adds "replace with this value", ignoring what was queued before.setPoints(50)acts likesetPoints(() => 50).
Keep updaters pure, and name them simply
React calls your updater functions itself, usually while it works out the next state, and sometimes right inside your set call. It may call one more than once: in development, StrictMode calls each updater twice and keeps one result, to help you spot an updater that does more than return a value. So an updater must be a pure function: it only works out the next value from the one it gets. This one breaks that rule:
setCount((c) => {
sendToServer(c + 1);
return c + 1;
});Line 2 sends a request from inside the updater. In development, StrictMode would send it twice. Put side effects in the event handler, and keep the updater to its one job:
function handleAdd() {
sendToServer(count + 1);
setCount((c) => c + 1);
}Line 2 sends the request once per click, from the handler. Line 3 only adds one. (Both blocks are pieces of a component, so they have no result of their own.)
For the updater's argument, pick a name that says what it is. Three habits are common: the first letter of the state variable, the full name, or the name with prev in front:
setCount((c) => c + 1);
setIsMuted((m) => !m);
setPoints((prevPoints) => prevPoints * 2);
setItems((items) => [...items, "Tea"]);Each row is a piece of state, an updater for it, and how to read it aloud:
| State | Updater | Read it as |
|---|---|---|
count | setCount((c) => c + 1) | Add one to the count. |
isMuted | setIsMuted((m) => !m) | Flip muted on or off. |
points | setPoints((prevPoints) => prevPoints * 2) | Double the points. |
items | setItems((items) => [...items, "Tea"]) | Add Tea to the end of the list. |
When should you write an updater? Whenever the next state is worked out from the current one and more than one update might be queued: several calls in one handler, or code that runs later, like a timer or the code after await. When you set a fixed value, like setNote(""), pass the value. Many teams write an updater every time the new state comes from the old one, so they never have to stop and check.
Uploads that finish later
A photo app shows how many uploads are running and how many are done. Each upload takes two seconds. Press Upload a photo three times quickly, then wait:
import { useState } from "react"; function upload() { return new Promise((resolve) => setTimeout(resolve, 2000));} export default function App() { const [uploading, setUploading] = useState(0); const [done, setDone] = useState(0); async function handleUpload() { setUploading(uploading + 1); await upload(); setUploading(uploading - 1); setDone(done + 1); } return ( <> <p>Uploading: {uploading}</p> <p>Done: {done}</p> <button onClick={handleUpload}>Upload a photo</button> </> );}While they run, it says Uploading: 3: each click comes after the last one rendered, so line 12 counts up from a fresh snapshot. But when they finish, it ends at Uploading: 1 and Done: 1, not 0 and 3 (on the way, it may even show -1). After the await on line 13, lines 14 and 15 still read the snapshot of their own click. The first click saw 0 uploading and 0 done, so it asks for -1 and 1. The second saw 1 and 0, so it asks for 0 and 1. The third saw 2 and 0: 1 and 1. The last replacement wins: 1 and 1.
Each finish should change whatever is queued by then, so write updaters:
import { useState } from "react"; function upload() { return new Promise((resolve) => setTimeout(resolve, 2000));} export default function App() { const [uploading, setUploading] = useState(0); const [done, setDone] = useState(0); async function handleUpload() { setUploading((u) => u + 1); await upload(); setUploading((u) => u - 1); setDone((d) => d + 1); } return ( <> <p>Uploading: {uploading}</p> <p>Done: {done}</p> <button onClick={handleUpload}>Upload a photo</button> </> );}Lines 12, 14, and 15 now pass functions. Each finish takes one away from the latest count and adds one to done, so three uploads end at Uploading: 0 and Done: 3.
setScore(5)setScore(score + 1)setScore((s) => s + 1)setIsOpen(!isOpen)setIsOpen((o) => !o)setItems([...items, "Tea"])setItems((list) => [...list, "Tea"])setScore(() => 0)
Sort each call: does it add a replacement value to the queue, or an updater function?
Common mix-ups
- "Every set function call renders the page right away." No. It queues an update. React renders once, after your handler finishes.
- "Batching only happens in click handlers." Not since React 18. Updates in timers, promises, and after
awaitare batched too. - "An updater gets this render's value." No. It gets the queued value: what the update before it returned, or the last render's state if it's first in the queue.
- "setCount(count + 1) and setCount((c) => c + 1) are the same." Only when it's the only update. With several in the queue, the first replaces with a fixed number, and the second builds on the queued value.
- "React batches two quick clicks together." No. Each click is its own event, and React renders after each one.
- "An updater is a good place to send a request." No. React may call it more than once; StrictMode calls it twice in development. Keep updaters pure.
Practice exercises
Write the queue down first: for each call, a replacement or a function. Then work through it, starting from the state the render had. Press Edit & run to check.
The count starts at 0. After one click, what does the button show?
import { useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
function handleClick() {
setCount((c) => c + 2);
setCount((c) => c + 2);
}
return <button onClick={handleClick}>Count: {count}</button>;
}import { useState } from "react"; export default function App() { const [count, setCount] = useState(0); function handleClick() { setCount((c) => c + 2); setCount((c) => c + 2); } return <button onClick={handleClick}>Count: {count}</button>;}The queue holds two updaters. The first gets 0 and returns 2; the second gets 2 and returns 4. The button shows Count: 4.
Points start at 1. After one click, what does the button show?
import { useState } from "react";
export default function App() {
const [points, setPoints] = useState(1);
function handleClick() {
setPoints((p) => p + 1);
setPoints(10);
setPoints((p) => p * 3);
}
return <button onClick={handleClick}>Points: {points}</button>;
}import { useState } from "react"; export default function App() { const [points, setPoints] = useState(1); function handleClick() { setPoints((p) => p + 1); setPoints(10); setPoints((p) => p * 3); } return <button onClick={handleClick}>Points: {points}</button>;}p => p + 1 turns 1 into 2. "Replace with 10" ignores the 2 and gives 10. p => p * 3 turns 10 into 30. The button shows Points: 30.
Buy a dozen adds only one egg. Fix line 8. After your fix, how many eggs are in the cart after one click?
import { useState } from "react"; export default function App() { const [eggs, setEggs] = useState(0); function handleBuyDozen() { for (let i = 0; i < 12; i++) { setEggs(eggs + 1); } } return ( <> <p>Eggs in cart: {eggs}</p> <button onClick={handleBuyDozen}>Buy a dozen</button> </> );}import { useState } from "react"; export default function App() { const [eggs, setEggs] = useState(0); function handleBuyDozen() { for (let i = 0; i < 12; i++) { setEggs((e) => e + 1); } } return ( <> <p>Eggs in cart: {eggs}</p> <button onClick={handleBuyDozen}>Buy a dozen</button> </> );}The loop queues twelve updaters. Each one adds one to the result of the one before, so one click gives 12, and the next gives 24.
Press Start: the time goes to 1 s and stays there. Fix it. After your fix, what does it show 3 seconds after you press Start?
import { useState } from "react"; export default function App() { const [seconds, setSeconds] = useState(0); function handleStart() { setInterval(() => { setSeconds(seconds + 1); }, 1000); } return ( <> <p>Time on this page: {seconds} s</p> <button onClick={handleStart}>Start</button> </> );}import { useState } from "react"; export default function App() { const [seconds, setSeconds] = useState(0); function handleStart() { setInterval(() => { setSeconds((s) => s + 1); }, 1000); } return ( <> <p>Time on this page: {seconds} s</p> <button onClick={handleStart}>Start</button> </> );}With an updater, each tick adds one to whatever the state is by then, so it shows 1, 2, 3. The interval's own snapshot doesn't matter any more.
Finish getFinalState in plain JavaScript, so it works through a queue the way React does. The first call should print 50. What does the second call print?
function getFinalState(baseState, queue) {
let finalState = baseState;
// For each update in the queue: a function gets finalState and
// returns the next one; any other value replaces finalState.
return finalState;
}
console.log(getFinalState(10, [15, (p) => p * 2, 50]));
console.log(getFinalState(2, [(p) => p + 1, (p) => p * 3]));function getFinalState(baseState, queue) {
let finalState = baseState;
for (const update of queue) {
if (typeof update === "function") {
finalState = update(finalState);
} else {
finalState = update;
}
}
return finalState;
}
console.log(getFinalState(10, [15, (p) => p * 2, 50]));
console.log(getFinalState(2, [(p) => p + 1, (p) => p * 3]));The loop does what React does with a queue: a function gets the current result and returns the next one; any other value replaces it. The first call prints 50, like the bonus example, and the second prints 9: 2 + 1 = 3, then 3 * 3 = 9.
Remove the last two only removes one. Fix it. After your fix, what is left in the cart after one click?
import { useState } from "react"; export default function App() { const [items, setItems] = useState(["Tea", "Cake", "Lemonade"]); function handleRemoveTwo() { setItems(items.slice(0, -1)); setItems(items.slice(0, -1)); } return ( <> <p>In your cart: {items.join(", ") || "nothing"}</p> <button onClick={handleRemoveTwo}>Remove the last two</button> </> );}import { useState } from "react"; export default function App() { const [items, setItems] = useState(["Tea", "Cake", "Lemonade"]); function handleRemoveTwo() { setItems((list) => list.slice(0, -1)); setItems((list) => list.slice(0, -1)); } return ( <> <p>In your cart: {items.join(", ") || "nothing"}</p> <button onClick={handleRemoveTwo}>Remove the last two</button> </> );}Each updater gets the list the one before returned. The first removes Lemonade, and the second removes Cake from that shorter list, so only Tea is left. slice makes a new array, so the state is never changed in place.
Check your understanding
For each code question, write down the queue first, then work through it from the starting value.
Question 1 of 8A click handler calls three different set functions. When does React render?
Choose an answer to see the explanation.
Question 2 of 8Starting from 0, what is
scoreafter one click with three updaters?Read the code, then predictimport { useState } from "react"; export default function App() { const [score, setScore] = useState(0); function handleClick() { setScore((s) => s + 2); setScore((s) => s + 2); setScore((s) => s + 2); } return <button onClick={handleClick}>Score: {score}</button>; }Choose an answer to see the explanation.
Question 3 of 8Starting from 0, what is
scoreafter one click with a value, then an updater?Read the code, then predictimport { useState } from "react"; export default function App() { const [score, setScore] = useState(0); function handleClick() { setScore(score + 2); setScore((s) => s + 2); } return <button onClick={handleClick}>Score: {score}</button>; }Choose an answer to see the explanation.
Question 4 of 8Starting from 0, what is
scoreafter one click with an updater, then a value?Read the code, then predictimport { useState } from "react"; export default function App() { const [score, setScore] = useState(0); function handleClick() { setScore((s) => s + 2); setScore(7); } return <button onClick={handleClick}>Score: {score}</button>; }Choose an answer to see the explanation.
Question 5 of 8Starting from 3, what is
scoreafter one click with two values?Read the code, then predictimport { useState } from "react"; export default function App() { const [score, setScore] = useState(3); function handleClick() { setScore(score + 1); setScore(score + 1); } return <button onClick={handleClick}>Score: {score}</button>; }Choose an answer to see the explanation.
Question 6 of 8In React 19, a
setTimeoutcallback calls three set functions. How many renders does that make?Choose an answer to see the explanation.
Question 7 of 8Which call adds an updater function to the queue?
Choose an answer to see the explanation.
Question 8 of 8Why must an updater function be pure?
Choose an answer to see the explanation.
Key takeaways
- A set function call doesn't render. It adds an update to a queue, and React renders once, after your handler finishes: batching.
- Since React 18, updates are batched everywhere: event handlers, timers, promises, and after
await. Separate clicks are never batched together, andflushSyncforces a render straight away. - An updater function, like
(c) => c + 1, gets the queued value and returns the next one. - Any other value means "replace with this value". The last update in the queue wins.
- Updaters must be pure, and their argument is usually named after the state:
c,count, orprevCount.
Remember the one-liner.
A value replaces the queued state; a function updates it; React works through the queue once, after your handler.
Coming next: Updating objects in state. It shows how to change one field of an object in state, like a profile's name, by making a new object instead of changing the old one.