React

Automatic batching of updates

Since React 18 updates batch inside promises, timeouts and native listeners too: two setState calls in the same tick produce a single render.

Key points

  • Before React 18 batching applied only inside React handlers; from 18 it applies everywhere.
  • flushSync forces the render immediately when code must read the layout right after the update.
  • A single render means the intermediate states never paint.

Since React 18 state updates batch inside promises, timeouts and native listeners too: two setState calls in the same tick produce one render instead of two.

What changed from React 17

Up to 17 the grouping applied only inside React handlers: a setTimeout or a then produced one render per update. From 18 the behaviour is identical in every context, including listeners added with addEventListener.

The async case

function Counter() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  async function handleClick() {
    await Promise.resolve();      // outside a React event handler
    setCount((c) => c + 1);
    setFlag((f) => !f);           // one render for both updates
  }

  return (
    
      {count} {String(flag)}
    
  );
}

In the code above the second render never happens: React applies both changes and paints once. The value read from the DOM after the await is still the previous one, because the render has not run yet.

When to switch batching off

flushSync forces the update before the next line. It is needed when the code must measure the layout right away, for instance to place a tooltip on the new height of an element.

import { flushSync } from "react-dom";

function addAndMeasure() {
  flushSync(() => setCount((c) => c + 1));
  const height = listRef.current.getBoundingClientRect().height;
  return height;
}

The cost is an extra render, so flushSync stays the exception. React also warns when you call it during render or inside a lifecycle, because a synchronous render is not allowed there.

The pattern that breaks

Reading state between two setState calls returns the old value: the closure variable does not change until the component runs again. There are two fixes, the functional update and a ref that carries the value outside the render.

The command

Automatic batching exists from 18; the command installs the current major and checks that the project did not stay on an earlier version.

npm install react@19 react-dom@19
npm ls react-dom

Live records in the catalogue

Three records that update several values at once: animated-counter animates a number across states, card-status-list changes state and badge in one action, project-data-table updates filter and page together. They are records among the 111,985 components and the 1,556 blocks.

Source and licence

React ships under the MIT licence. The version 18 release notes describe automatic batching as the default behaviour, with no flag to enable, and react.dev documents flushSync as an escape hatch.

The limitation

Batching cuts the number of renders, not the work per render: an expensive computation in the component body runs once with the final state, so the cost stays. In development StrictMode calls some functions twice, and counters in logs look wrong.

Where to continue

The major version changes are in React 19; the double calls in development are in StrictMode; the comparison with the Vue model is in Vue 3 vs React.

Frequently asked questions

Does batching apply to updates inside useEffect?

Yes from 18: updates scheduled in an effect batch with those of the same cycle. Before 18 each one produced a separate render.

Does one render mean one commit?

Yes, one render leads to one commit. Batching reduces both, but it does not reduce the work done by computations inside the component.

When do I need flushSync?

When the code must read the updated DOM right after the setState, for instance to measure an element before placing another. The normal case needs nothing.

Does batching break tests?

No, but it changes how many renders the test sees. A test that counts calls inside the component body sees fewer, because the component runs once.

Does the behaviour change in React 19?

No, React 19 keeps the automatic batching introduced in 18. Other things change, such as ref as a prop and actions, not this.