Skip to the solution
MediumFrontend6 min read

How does React's Batching work?

905 views

The problem

Question presented to candidate: "If you call three setters in one event handler, how many times does the component render? And does that change inside a setTimeout?"

What a strong answer should cover:

  • Batching groups multiple state updates into a single re-render, so the UI never shows a half-applied intermediate state.
  • Automatic batching since React 18 applies everywhere β€” event handlers, setTimeout, promises, native event listeners. Before 18 it only applied inside React event handlers.
  • That was one of the more visible behavioural changes in React 18, and a common source of "this used to render twice" surprises.
  • State updates are asynchronous in the sense that the variable does not change until the next render β€” reading it immediately after a setter gives the old value.
  • The functional updater is how you compose several updates to the same value: setN(n => n + 1) three times increments by three, where setN(n + 1) three times increments by one.
  • flushSync opts out, forcing a synchronous render β€” an escape hatch for measuring the DOM between updates, at the cost of an extra render and lost batching.
  • Why it exists: fewer renders, and no intermediate frames where two related pieces of state disagree.

Clarifying questions expected:

  • "Which React version?" β€” the answer genuinely differs before and after 18.
  • "Are the updates to the same value or different ones?" β€” that decides whether a functional updater is needed.

Code / implementation expected: Yes β€” updates in a handler, in a timeout, and with a functional updater versus a stale read.

  • state
  • performance
  • rendering

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

6 min read Β· 9 sections

Try it in the playground

Open any example in the playground to run and change it.

ReactCounting renders across handlers, timeouts and promises
import { useState, useRef } from "react";
import { flushSync } from "react-dom";

export default function App() {
  const [a, setA] = useState(0);
  const [b, setB] = useState(0);
  const [naive, setNaive] = useState(0);
  const [functional, setFunctional] = useState(0);
  const [log, setLog] = useState([]);

  const renders = useRef(0);
  renders.current++;
  const renderAtStart = useRef(0);

  const mark = (label) => {
    const used = renders.current - renderAtStart.current;
    setLog((l) => [...l.slice(-4), label + " -> " + used + " render(s)"]);
  };
  const begin = () => { renderAtStart.current = renders.current; };

  // Three updates in a handler: one render.
  const inHandler = () => {
    begin();
    setA((x) => x + 1); setB((x) => x + 1); setA((x) => x + 1);
    setTimeout(() => mark("3 updates in a handler"), 0);
  };

  // Since React 18 this is ALSO one render. On React 17 it was three.
  const inTimeout = () => {
    begin();
    setTimeout(() => {
      setA((x) => x + 1); setB((x) => x + 1); setA((x) => x + 1);
      setTimeout(() => mark("3 updates in setTimeout"), 0);
    }, 0);
  };

  // Also one render.
  const inPromise = () => {
    begin();
    Promise.resolve().then(() => {
      setA((x) => x + 1); setB((x) => x + 1); setA((x) => x + 1);
      setTimeout(() => mark("3 updates in a promise"), 0);
    });
  };

  // flushSync forces each update through immediately β€” batching opted out.
  const withFlush = () => {
    begin();
    flushSync(() => setA((x) => x + 1));
    flushSync(() => setB((x) => x + 1));
    setTimeout(() => mark("2 updates with flushSync"), 0);
  };

  // The classic bug: all three read the SAME naive value from this render.
  const bumpNaive = () => { setNaive(naive + 1); setNaive(naive + 1); setNaive(naive + 1); };
  // The fix: each update applies to the result of the previous one.
  const bumpFunctional = () => {
    setFunctional((n) => n + 1); setFunctional((n) => n + 1); setFunctional((n) => n + 1);
  };

  const btn = { marginRight: 6, marginBottom: 6 };

  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6, maxWidth: 520 }}>
      <p style={{ fontSize: 14 }}>
        component renders: <strong>{renders.current}</strong> Β· a={a} b={b}
      </p>

      <p>
        <button onClick={inHandler} style={btn}>handler</button>
        <button onClick={inTimeout} style={btn}>setTimeout</button>
        <button onClick={inPromise} style={btn}>promise</button>
        <button onClick={withFlush} style={btn}>flushSync</button>
      </p>

      <pre style={{ background: "#f6f6f6", padding: 10, borderRadius: 6, fontSize: 12 }}>
        {log.length ? log.join("\n") : "(press a button)"}
      </pre>

      <h4 style={{ margin: "12px 0 6px" }}>Three increments, two ways</h4>
      <p style={{ fontSize: 14 }}>
        <button onClick={bumpNaive} style={btn}>setNaive(naive + 1) x3</button>
        <strong>{naive}</strong> β€” climbs by 1
      </p>
      <p style={{ fontSize: 14 }}>
        <button onClick={bumpFunctional} style={btn}>setFunctional(n =&gt; n + 1) x3</button>
        <strong>{functional}</strong> β€” climbs by 3
      </p>
    </div>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 43 of 119 in React.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All React.js questions
Up nextMedium

How do you test React components?

Continue