cf.completefrontend
Open lab My progress
THE REACT FIELD GUIDE

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.

By the end, you can
  • 01
    Explain batchingSay why React renders once after a handler that sets state several times, in clicks, timers, and after await, and what flushSync changes.
  • 02
    Use 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.
  • 03
    Mix replacing and updatingPredict the result when a handler mixes setPoints(50) with setPoints((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:

The handler from State as a snapshot: three requests for the same numberJavaScript
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:

Add 3 teas with updater functionsPop out in the code editor (opens in a new tab)JSX
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>    </>  );}
ResultLoading…

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.

Left, a window, the click, holding three cards, each setCount((c) => c + 1). An arrow leads to a crate labelled queue holding three function cards, each (c) => c + 1. Another arrow leads to a window, render, holding 0 + 1 = 1, 1 + 1 = 2, and 2 + 1 = 3. Under it: next render: count is 3.the clicksetCount((c) => c + 1)setCount((c) => c + 1)setCount((c) => c + 1)your handler runs(c) => c + 1(c) => c + 1(c) => c + 1queueReact queues themrender0 + 1 = 11 + 1 = 22 + 1 = 3next render: count is 3
The click puts three updater functions in the queue and changes nothing yet. When the handler is done, React calls them in order, each with the result of the one before: 0 becomes 1, then 2, then 3. The next render has count 3.
Real-life analogyAn order pad at a café

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
The main idea

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:

Place an order: three pieces of statePop out in the code editor (opens in a new tab)JSX
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>    </>  );}
ResultLoading…

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.

Left, batched: a window, the click, with three updates: setItems(0), setStatus, setNote. One arrow leads to a single card, 1 render. Right, with flushSync: the same three updates, each with its own arrow to its own render card, 1, 2 and 3, labelled 2 half-finished screens.batchedthe clicksetItems(0)setStatus(…)setNote(…)1renderwith flushSyncthe clicksetItems(0)setStatus(…)setNote(…)123renders2 half-finished screens
Left: React waits until the handler is done, then makes one render with all three changes. Right: 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.

Playground: how many renders?
The handler for your choiceJavaScript
function handlePlaceOrder() {
  setItems(0);
  setStatus("Order placed");
  setNote("We'll email your receipt.");
}
The checkout0 renders

Items: 2

Status: In your cart

Note: (empty)

Renders for Place order

None yet.

Try it yourself

Pick where the updates run, then press Place order in the checkout.

The checkout runs here, with React on this page. Pick where its three updates run, then press Place order. The list shows every render React committed for them, with what the page showed. Back to the cart puts the checkout back. Reset starts again.

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 updates run, and how many renders they make
Where the updates runBatched in React 18 and later?Renders for three updates
In a click handlerYes.One render, after the handler finishes.
In a setTimeout callbackYes. Before React 18, no.One render, after the callback finishes.
After await, or in a promise's thenYes. Before React 18, no.One render, after that code finishes.
Each one inside its own flushSyncNo. flushSync renders straight away.Three renders, one per update.
Two separate clicksNo. 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:

  1. 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.)
  2. 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.
  3. It hands each updater's result to the next one, as its argument.
  4. The last result is the new state, and that's what useState returns 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:

Add 3 teas: React works through the queue
Queued updatec it getsWhat it returns
c => c + 100 + 1 = 1
c => c + 111 + 1 = 2
c => c + 122 + 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.

Playground: work through the queue
The handler you builtJavaScript
function handleClick() {
  setPoints(points + 5);
  setPoints((p) => p * 2);
}
The pageclick Run handleClick

Points: 10

How React worked through the queue

Queued updatep it getsIt returns
Run the handler first.
Try it yourself

Add calls to handleClick with the buttons, then press Run handleClick on the page.

The points counter runs here, with React on this page, starting at 10. Each button adds one call to handleClick, up to 5. Run handleClick calls them in one click; the table replays how React worked through the queue, one row per Step. Reset puts the points back to 10.

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:

A bonus, then double pointsPop out in the code editor (opens in a new tab)JSX
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>    </>  );}
ResultLoading…

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:

A bonus, then double points
Queued updatep it getsWhat it returns
"replace with 15"10 (not used)15
p => p * 21515 * 2 = 30

Now add one more line at the end, a fixed number:

A bonus, double points, then a fixed 50Pop out in the code editor (opens in a new tab)JSX
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>    </>  );}
ResultLoading…

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:

A bonus, double points, then a fixed 50
Queued updatep it getsWhat it returns
"replace with 15"10 (not used)15
p => p * 21515 * 2 = 30
"replace with 50"30 (not used)50
The queue in a row: replace with 15, the function (p) => p * 2, and replace with 50. Below, the values: start 10, then 15, then 30, then 50 for the next render. Arrows lead from each value into the next queued update and down to its result.queuereplace with 15(p) => p * 2replace with 5010start153050next render
Points start at 10. "Replace with 15" ignores the 10 and gives 15. (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 50 or points + 5, adds "replace with this value", ignoring what was queued before. setPoints(50) acts like setPoints(() => 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:

An updater with a side effect (don't do this)JavaScript
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:

The request in the handler, a pure updaterJavaScript
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:

Ways to name an updater's argumentJavaScript
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:

Updater names
StateUpdaterRead it as
countsetCount((c) => c + 1)Add one to the count.
isMutedsetIsMuted((m) => !m)Flip muted on or off.
pointssetPoints((prevPoints) => prevPoints * 2)Double the points.
itemssetItems((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:

An upload counter that reads its snapshot (a bug on purpose)Pop out in the code editor (opens in a new tab)JSX
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>    </>  );}
ResultLoading…

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:

The same counter with updater functionsPop out in the code editor (opens in a new tab)JSX
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>    </>  );}
ResultLoading…

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.

Replace or update?
  • setScore(5)
  • setScore(score + 1)
  • setScore((s) => s + 1)
  • setIsOpen(!isOpen)
  • setIsOpen((o) => !o)
  • setItems([...items, "Tea"])
  • setItems((list) => [...list, "Tea"])
  • setScore(() => 0)
Try it yourself
0 of 8 correct

Sort each call: does it add a replacement value to the queue, or an updater function?

Choose a category for every card. You can change an answer at any time; Reset clears them all.

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 await are 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.

Exercise 1 · Warm-upTwo updaters

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>;
}

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upUpdater, value, updater

    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>;
    }

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeBuy a dozen

      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>    </>  );}
      ResultLoading…

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeA timer stuck at 1

        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>    </>  );}
        ResultLoading…

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeWrite the queue yourself

          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?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          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]));

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeRemove the last two

            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>    </>  );}
            ResultLoading…

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each code question, write down the queue first, then work through it from the starting value.

              The Queueing state updates quiz · 8 questionsScore: first tries count
              1. Question 1 of 8A click handler calls three different set functions. When does React render?

                Choose an answer to see the explanation.

              2. Question 2 of 8Starting from 0, what is score after one click with three updaters?

                Read the code, then predictPop out in the code editor (opens in a new tab)JSX
                import { 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.

              3. Question 3 of 8Starting from 0, what is score after one click with a value, then an updater?

                Read the code, then predictPop out in the code editor (opens in a new tab)JSX
                import { 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.

              4. Question 4 of 8Starting from 0, what is score after one click with an updater, then a value?

                Read the code, then predictPop out in the code editor (opens in a new tab)JSX
                import { 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.

              5. Question 5 of 8Starting from 3, what is score after one click with two values?

                Read the code, then predictPop out in the code editor (opens in a new tab)JSX
                import { 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.

              6. Question 6 of 8In React 19, a setTimeout callback calls three set functions. How many renders does that make?

                Choose an answer to see the explanation.

              7. Question 7 of 8Which call adds an updater function to the queue?

                Choose an answer to see the explanation.

              8. 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, and flushSync forces 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, or prevCount.

              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.

              CompleteFrontend Clear concepts. Working examples.