skip to content

In a React 19 component, one click handler calls setCount, setName and setOpen one after another, yet the component body logs only one render. What is automatic batching, and why does React work this way?

level: juniorimportance: must knowfreq 72%

answer

  1. setters queue, they do not render
  2. one logical action, one render pass
  3. intermediate UI nobody should see
  4. the batch closes when the work returns
  5. escape hatch exists but is rare

basics

~20 s

Automatic batching means React collects every state update queued while the current work runs and applies them in a single re-render instead of rendering once per setter. It avoids wasted renders and half-updated intermediate UI.

solid answer

~50 s

Calling a state setter does not render immediately — it queues an update on the component. React waits until the currently running work (the event handler, in this case) finishes, then performs one render pass that reflects all three updates at once. That is automatic batching. The motivation is both performance and correctness: three renders would do three times the reconciliation and commit work for a UI nobody ever sees, and the intermediate renders would show a state combination that never logically existed — a new `count` next to the old `name`. Batching also spans components: if the handler updates state in a parent and in a child, React still re-renders the affected tree once. It does not merge the values into a single object the way class `this.setState` did, and it does not make the setters synchronous — the DOM is not updated until after the handler returns.

code

jsx · 18 lines
jsx
import { useState } from 'react';

export default function Panel() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('anon');
  console.log('render', count, name);

  function handleClick() {
    setCount(count + 1);
    setName('ada');
  }

  return (
    <button onClick={handleClick}>
      {name}: {count}
    </button>
  );
}

go deeper

for a junior

Be ready to say plainly that setters queue updates and React re-renders once when the handler finishes, and to predict the render count for a handler that calls three setters.

for a middle

Explain where the batch boundary sits — the end of the running work, not the end of each setter — and why an unbatched intermediate render would expose a state combination the UI was never designed for.

for a senior

Show that you reason about batching when debugging: unexpected render or effect counts, code that reads the DOM too early, and the rare place where forcing a synchronous commit is genuinely justified.

for a principal

Frame batching as a scheduling contract the framework owns: application code should state intent and let React choose when to flush, and every opt-out you allow in the codebase is a place that constrains future scheduling work.

## The mental model A React state setter is a *request*, not an assignment. When you call `setCount(c + 1)`, React records an update against that component and schedules work; it does not stop and re-render on the spot. Automatic batching is the rule that decides *when* that scheduled work actually runs: React waits until the synchronous code that queued the updates has finished, and then renders once with all of them applied. So a handler like this produces exactly one render: ```jsx function Panel() { const [count, setCount] = useState(0); const [name, setName] = useState(''); const [open, setOpen] = useState(false); console.log('render', count, name, open); function handleClick() { setCount(count + 1); // queued setName('ada'); // queued setOpen(true); // queued } // handler returns -> React renders once return <button onClick={handleClick}>go</button>; } ``` The `console.log` in the body fires twice in total across the interaction: once for the initial render, once after the click. Never three times. ## Why React does it **Performance.** Each render pass means re-running the component functions in the affected subtree, diffing the resulting elements against the previous tree, and committing the differences to the DOM. Doing that three times for one logical user action triples the work, and two thirds of it produces pixels the user never sees, because the browser cannot paint in the middle of your synchronous handler anyway. **Consistency.** This is the part candidates usually miss. Without batching, an intermediate render would exist in which `count` had been updated but `name` had not. That combination is not a state your UI was ever designed to be in. Anything reading both — a derived value, a conditional, a child that receives both as props — would briefly see an incoherent pair. Batching means every render observes a *coherent snapshot*: all the updates from one logical action, applied together. **Scope.** The batch is not per-component. If a click handler calls a setter for the parent and a setter passed down to a child, React still does a single render pass over the affected part of the tree. It is one pass over the work, not one pass per state variable. ## Three things batching is not **It is not object merging.** In legacy class components, `this.setState({a: 1})` shallow-merged a partial object into `this.state`. With `useState` there is no merge — each setter owns one independent value. Batching is about *how many renders happen*, not about combining values into one object. **It does not make the setter synchronous.** The DOM has not changed when the setter returns, and it has not changed when the handler returns either — React renders and commits after the handler, before the browser paints. Code that reads the DOM immediately after a setter sees the old DOM. **It is not limited to event handlers.** Since React 18 (and therefore in React 19) React batches updates wherever they are queued — inside a promise callback, a `setTimeout`, a handler added with `addEventListener`. Older React batched only inside its own event handlers, which is why upgrade guides talk about "automatic" batching: the automatic part is that you no longer have to do anything to get it. ## Opting out Occasionally you need the DOM updated *before* the next line of imperative code runs — for example you append an item and then immediately want to scroll it into view. `flushSync` from `react-dom` forces React to render and commit the updates queued inside its callback before it returns. It is an escape hatch: it costs a synchronous render and gives up the scheduling flexibility batching buys you, so it is rare in application code. ## What interviewers listen for A weak answer says "React is asynchronous" and stops. A good answer names the two payoffs — fewer render passes and no incoherent intermediate state — states that the batch boundary is the end of the running work rather than the end of each setter, and knows the escape hatch exists without reaching for it by default.

  • Does batching mean the DOM is already updated by the time the click handler returns?
    No. React renders and commits after the handler finishes, before the browser paints. Inside the handler — and on the line right after the last setter — the DOM still shows the previous output. If you truly need the committed DOM before continuing, `flushSync` from `react-dom` is the only supported way to force it.
  • If a handler updates state in a parent and in a child, is that one render or two?
    One render pass. Batching is scoped to the scheduled work, not to a component, so React reconciles the affected part of the tree once with both updates applied. That is also why batching improves consistency: the parent and child never render out of step with each other.
  • Does batching change how many times my effects run?
    Yes, and that is usually what you want. Effects run after the commit, so one batched render means one round of effect cleanup and re-run instead of three. Code that assumed an effect would fire once per setter is relying on unbatched behavior, which is a bug rather than a feature.

It is like a waiter taking every order at the table before walking to the kitchen, instead of making one trip per dish. Fewer trips, and the whole table is served a consistent meal.

saying these in an interview costs you the question

  • Says setState is asynchronous because it returns a promise
  • Claims batching merges the state values into one object
  • Expects the DOM to be updated on the line after the setter
  • Thinks each useState setter always triggers its own render
  • Believes batching only ever applies inside React event handlers

context