Understanding React 18's Automatic Batching and When to Opt Out
Learn how React 18 groups state updates for fewer renders, see a concrete example with setTimeout, and discover how to verify or disable the behavior when needed.
04 Nov 2025, 12:54 UTC

The problem: extra renders hurting performance
In a typical React app, multiple state changes that happen close together can cause the component to render more times than necessary. Each render triggers a reconciliation pass, which consumes CPU and may cause visible flicker if intermediate states are shown to the user. Before React 18, this batching only occurred inside React‑originated event handlers (like onClick). Updates scheduled from setTimeout, promises, or native DOM events were processed separately, leading to extra renders.
Thesis: automatic batching in React 18 reduces wasted work by default
React 18 introduces automatic batching: any state updates that happen within the same tick of the JavaScript event loop are grouped into a single render, regardless of where they originate. This means that code wrapped in setTimeout, Promise.then, or a native event listener now benefits from the same optimization that React event handlers enjoyed.
How it works
When React processes updates, it checks whether a render is already scheduled for the current tick. If so, it defers the new state changes and adds them to the same batch. At the end of the tick, React performs one render with the combined state. This reduces the number of render passes from N to 1 for N updates that occur in the same tick.
Worked example: observing batching with setTimeout
Consider a component that tracks a counter. Two state setters are called inside a setTimeout callback. With automatic batching enabled, the counter should increase by 1 after the timeout, not 2, because both updates are flushed together.
import { useState } from 'react';
import { createRoot } from 'react-dom/client';
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setTimeout(() => {
setCount(c => c + 1); // first update
setCount(c => c + 1); // second update
}, 0);
};
return (
Count: {count}
Increment twice
);
}
const container = document.getElementById('root');
const root = createRoot(container);
root.render();
To verify the behavior, you can log the value inside the setter functions or use useEffect to watch count. After clicking the button, the effect should run once and the displayed count should go from 0 to 1.
Trade‑off: when you need to see intermediate state
Automatic batching can hide bugs that relied on seeing an intermediate render (for example, a component that reads a value from the DOM after the first state change). If your code depends on that intermediate state, you may observe incorrect behavior after upgrading to React 18.
React provides an escape hatch: ReactDOM.flushSync. Wrapping a state update in flushSync forces React to flush the queue and render immediately, bypassing the automatic batch for that update.
Using flushSync to opt out
Modify the previous example so that only the first setter runs synchronously:
import { flushSync } from 'react-dom';
const handleClick = () => {
setTimeout(() => {
flushSync(() => {
setCount(c => c + 1); // forced flush, renders now
});
setCount(c => c + 1); // batched with the next tick
}, 0);
};
Now the component will render twice: once after the flushed update (count goes from 0 to 1) and again after the batched update (count goes from 1 to 2). You can confirm this by observing two effect runs or two updates to the DOM.
Limitations and verification checklist
- Automatic batching only applies to updates that occur in the same event‑loop tick. Updates separated by another tick (e.g., two separate
setTimeoutcalls) will still cause separate renders. - If you are still using the legacy
ReactDOM.renderAPI, automatic batching is not enabled. Migrate tocreateRootto get the new behavior. - Third‑party libraries that manually call
ReactDOM.renderor rely on the old batching may needflushSyncor an update to their code.
To verify that your app is using automatic batching:
- Confirm you are on React 18 or later (
npm list react). - Ensure the root is created with
ReactDOM.createRoot. - Run the setTimeout example above and check that the counter increments by one per click.
- Optionally wrap one setter in
flushSyncand confirm the counter increments by two.
Actionable closing
Automatic batching is a welcome performance improvement that requires no code changes for most applications. After upgrading to React 18, test your components to ensure they do not depend on intermediate render states. If you encounter a case where immediate rendering is necessary, use flushSync sparingly—only around the specific updates that need to break out of the batch. This approach lets you keep the benefits of batching while retaining control when the behavior truly matters.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.