How React batches multiple setState calls into a single re-render for performance.
State batching means React collects multiple setState calls from the same synchronous block and processes them all in a single re-render pass. Instead of re-rendering after each individual setState, React waits until the end of the current 'batch', applies all updates at once, and renders exactly once. React 18 extended automatic batching to cover async code and native event listeners too.
When you call setA(1) and setB(2) inside a click handler, React does not immediately re-render after setA. It queues both updates and schedules a single re-render. Your component renders once with both new values applied.
Each setState call appends an Update object to the hook's circular queue. React marks the fiber as having pending work and schedules a microtask (or callback) to flush the queue β but only after the current synchronous code finishes.
In React 17, batching only happened inside React event handlers. setState calls inside setTimeout, Promises, or fetch callbacks triggered individual re-renders. React 18 batches ALL updates automatically, regardless of origin.
If you need a synchronous DOM update (e.g., to measure layout after a state change), call ReactDOM.flushSync(() => setState(...)). This forces React to flush the update immediately and synchronously, bypassing batching.
When multiple setState calls depend on the previous value, use the functional form: setState(prev => prev + 1). React applies these updates in queue order, so each update sees the result of the previous one β not the value at render time.
React 18 feature: all setState calls are batched together, even inside setTimeout, Promises, and native event listeners. Reduces unnecessary re-renders.
A circular linked list on each hook object that stores pending setState calls. Flushed at the end of each batch β all updates applied in order.
Forces React to flush pending state updates synchronously and immediately. Bypasses batching. Use only when you need a DOM measurement after a state change.
setState(prev => newValue). Guaranteed to receive the latest state value, even when updates are batched or enqueued asynchronously.
Batching reduces render count. Without batching: N setState calls = N renders. With batching: N setState calls = 1 render.
1// React 18: ALL of these batch into 1 render23// Inside event handler (always batched)4function handleClick() {5 setCount(c => c + 1); // queued6 setFlag(true); // queued7 setName('Alice'); // queued8 // 1 render happens here9}1011// Inside setTimeout β NOW also batched in React 1812setTimeout(() => {13 setCount(c => c + 1); // queued14 setFlag(true); // queued15 // 1 render (React 17 would have triggered 2!)16}, 0);1718// Opt out of batching when needed:19import { flushSync } from 'react-dom';20flushSync(() => setCount(1)); // renders immediately21flushSync(() => setFlag(true)); // renders again22// 2 renders total
Batching is one of React's most important performance optimizations. Without it, a single user interaction that triggers 3 state updates would cause 3 full render + reconcile + commit cycles. With batching, it's always just 1. React 18's automatic batching is especially valuable for async code β server responses, WebSocket messages, and timers no longer thrash the renderer.
A login form validates email, password, and shows/hides error messages. The submit handler calls `setEmailError(...)`, `setPasswordError(...)`, `setIsSubmitting(false)` β three state updates that should be a single render.
In React 17, calling setState inside setTimeout or fetch().then() didn't batch β each call triggered a separate render. Users would see a flash: first the email error appears, then the password error, then the loading spinner disappears. Three renders for one validation check.
React 18's automatic batching ensures all three setState calls result in ONE render, regardless of whether they're inside an event handler, setTimeout, fetch callback, or promise. The user sees all errors and the stopped spinner simultaneously β no flash.
Takeaway: React 18's automatic batching is a free performance upgrade for existing code. If you migrated from React 17 and had flushSync workarounds for batching, you can now remove them β batching works everywhere automatically.
You need to update a component's state and immediately measure the resulting DOM height to pass to a third-party animation library. With batching, the DOM hasn't updated yet when you try to measure.
Automatic batching defers the DOM update. After calling `setExpanded(true)`, the DOM still shows the collapsed state. Measuring `element.offsetHeight` returns the old height. The animation library receives the wrong value.
Wrap the state update in `flushSync()`: `flushSync(() => setExpanded(true))`. This forces React to immediately commit the DOM update synchronously. The next line can safely measure `element.offsetHeight` and get the expanded height. Use sparingly β it opts out of batching's performance benefit.
Takeaway: flushSync is the escape hatch when you need synchronous DOM access after a state update. Common use cases: third-party animation libraries, scroll position restoration, and focus management. Use it only when batching conflicts with imperative DOM operations.