Batching State Updates
How React 18+ batches setState in events, timeouts, and promises: when updates flush and why double setState still works.
- react
- batching-state
Click a button that calls setCount(c => c + 1) twice. You still get +2, and React only re-renders once. That is batching: multiple state updates scheduled in the same turn get applied together before paint.
Before React 18, batching was mostly limited to React event handlers. Updates inside setTimeout, promises, or native listeners often flushed immediately — one render per setState. React 18’s automatic batching covers those paths in concurrent roots (createRoot).
Docs: Queueing a Series of State Updates, Automatic batching.
Mental model
State updates are requests, not synchronous mutations. When you call setX, React:
- Schedules an update on the fiber.
- In the same batch window, may receive more
set*calls. - On flush, applies all queued updates for that fiber, then re-renders once.
Functional updates (setC(c => c + 1)) chain correctly inside a batch because each updater receives the previous pending value, not a stale closed-over number.
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount((c) => c + 1);
setCount((c) => c + 1);
// One re-render; count becomes previous + 2
}
return <button onClick={handleClick}>{count}</button>;
}
Replace with setCount(count + 1) twice and you only get +1 — both reads see the same render’s count. That is not a batching bug; it is the classic stale-value trap.
Automatic batching outside events
function Search() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
async function run(q) {
setLoading(true);
setQuery(q);
const data = await fetch(`/api/search?q=${encodeURIComponent(q)}`).then((r) => r.json());
setResults(data.items);
setLoading(false);
// After await, updates in the same synchronous stretch still batch under React 18
}
return (
<>
<input onChange={(e) => run(e.target.value)} value={query} />
{loading ? <p>Loading…</p> : <List items={results} />}
</>
);
}
You still get intermediate paints when awaits yield to the browser — batches are per task, not “the whole async function is one atomic UI transaction.”
Opting out with flushSync
Rarely you need the DOM to reflect state before the next line (measuring layout, integrating a library that reads the DOM immediately):
import { flushSync } from 'react-dom';
flushSync(() => {
setOpen(true);
});
const h = panelRef.current.getBoundingClientRect().height;
flushSync forces a synchronous render/commit. Overuse destroys performance. Prefer layout effects or redesigning the flow. See flushSync rare cases.
Footguns
| Mistake | What happens |
|---|---|
| Expecting one batch across multiple macrotasks | Each timeout/promise stretch flushes on its own |
| Using value form of setState twice in one handler | Second write overwrites first; use functional updates |
| Wrapping everything in flushSync “to be safe” | Extra commits, jank, harder concurrent reasoning |
| Assuming class setState merges for hooks | Hooks replace the value you pass (objects need spreads) |
Interview out-loud
“React batches multiple setState calls in the same event or discrete update window so we re-render once. Functional updaters chain correctly inside a batch. React 18 auto-batches more than 17, including timeouts and promises. flushSync opts out when you must read the DOM immediately after a state change.”
Related on this site
Further reading
Related guides
- Accessibility Patterns in ReactPractical React a11y: labels, focus management, keyboard, live regions, and composition patterns that stay accessible.
- Avoid Prop Drilling with CompositionStop threading props through intermediates: children slots, inversion of control, and when context is the right escape hatch.
- Children Prop PatternsUsing children and slot props for flexible APIs: wrappers, compound components, and when to prefer explicit props.
- Client Component BoundariesWhere to put use client: push interactivity to leaves, serializable props, and children as server slots.
- Client Routing Mental ModelClient-side routing updates the URL and UI without full reloads: history API, link interception, and data loading.