Suggest an editImprove this articleRefine the answer for “What is batching in React?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Batching** in React groups multiple state updates into a single re-render to skip intermediate renders. ```jsx function handleClick() { setCount(c => c + 1); setFlag(f => !f); // Both queued → one render, not two } ``` **Key point:** React 18 auto-batches in all contexts (promises, timeouts). React 17 batched only inside React event handlers.Shown above the full answer for quick recall.Answer (EN)Image**Batching** in React is grouping multiple state updates into a single re-render to skip unnecessary intermediate renders. ## Theory ### TL;DR - Batching is like a chef who preps all ingredients before turning on the stove: multiple `setState` calls happen fast, but the render fires once. - Without batching, 3 `setState` calls = 3 renders. With it, 3 calls = 1 render. - React 17 batched only inside React event handlers. React 18 batches everywhere, including promises and timeouts. - `flushSync` is not a batching tool. It breaks batching and forces a synchronous render immediately. ### Quick example ```jsx function Counter() { const [count, setCount] = useState(0); const [flag, setFlag] = useState(false); const handleClick = () => { setCount(c => c + 1); // queued setFlag(f => !f); // queued // React batches both → single re-render }; return <button onClick={handleClick}>Count: {count}, Flag: {flag}</button>; } ``` Both updates are queued together. React commits them in one pass. The component renders once, not twice. ### React 17 vs React 18 In React 17, batching only worked inside React-controlled events like `onClick` or `onChange`. Move those same `setState` calls into a `setTimeout` or a `Promise.then` and you got two separate renders. This was a quiet source of performance issues in async-heavy code. React 18 changed this with a new scheduler model. Batching now applies in all contexts: event handlers, async callbacks, timeouts, intervals. The behavior is consistent regardless of where the update originates. ```jsx // React 17: 2 renders. React 18: 1 render. function PromiseUpdater() { const [a, setA] = useState(0); const [b, setB] = useState(0); const trigger = () => { Promise.resolve().then(() => { setA(1); // React 17: renders here setB(2); // React 17: renders again; React 18: batched }); }; return <button onClick={trigger}>A: {a}, B: {b}</button>; } ``` ### When to use - Multiple `setState` calls in the same event handler: nothing to do, automatic in both React 17 and 18. - State updates inside `Promise.then` or `setTimeout` on React 18+: still automatic. - State updates inside `Promise.then` on React 17: wrap in `unstable_batchedUpdates` from `react-dom`. - Need to read the DOM synchronously right after a state change: use `flushSync`, but only there. ### How batching works internally React collects state updates into a queue (part of `react-reconciler`) within a single update lane. Instead of calling `commitRoot` after every `setState`, it waits for the current scope to finish, then flushes the entire queue in one pass and renders once. In React 18, the scheduler uses a priority-based lanes model that separates urgent updates (user input) from deferred ones (transitions). That is why `useTransition` can defer low-priority work without affecting batching on the urgent path. ### Common mistakes **Mistake 1: Assuming batching works everywhere in React 17.** ```jsx // React 17: 2 renders setTimeout(() => { setA(1); // renders setB(2); // renders again }, 0); // Fix for React 17 import { unstable_batchedUpdates } from 'react-dom'; setTimeout(() => { unstable_batchedUpdates(() => { setA(1); setB(2); }); }, 0); ``` The fix works. But `unstable_batchedUpdates` carries that prefix for a reason. Upgrading to React 18 removes the problem entirely. **Mistake 2: Using `flushSync` to "enable" batching.** It does the opposite. `flushSync` breaks batching and forces a synchronous render on the spot. Use it only when you need a DOM measurement before the next frame runs. ```jsx flushSync(() => setCount(1)); // synchronous render here const rect = ref.current.getBoundingClientRect(); // safe to measure now ``` **Mistake 3: Expecting `startTransition` updates to batch with urgent updates.** `useTransition` marks updates as low-priority. React may flush them in a separate pass from urgent updates. A batched urgent update and a deferred transition update can land in different render cycles. That is by design, not a bug. ```jsx const [isPending, startTransition] = useTransition(); startTransition(() => { setFilter('done'); // low-priority, may render separately }); ``` **Mistake 4: Using StrictMode to test batching behavior.** StrictMode mounts components twice in development only. It does not simulate production batching. Use React DevTools Profiler to count actual render commits. ### Real-world usage - Redux Toolkit: `batch` from `react-redux` wraps multiple dispatches to trigger one store re-render. - React Query: `useMutation` batches optimistic updates so forms do not flicker on submit. - Next.js: `useSWR` mutations auto-batch during SSR hydration in React 18. - Zustand: `batch(fn)` handles multi-store updates in one render. ### Follow-up questions **Q:** How did batching change from React 17 to React 18? **A:** React 17 batched only inside React event handlers. React 18 auto-batches in all contexts (promises, timeouts, intervals) using a new lanes-based scheduler. **Q:** When does batching NOT happen? **A:** In React 17, outside of React events. In any version, when you call `flushSync` it forces a synchronous render and exits the batch queue immediately. **Q:** What is `unstable_batchedUpdates` and should you use it? **A:** It is a legacy API that forces batching outside React events in React 17. Use it only if upgrading to React 18 is not an option. The "unstable" prefix is intentional. **Q:** How does batching interact with `useTransition`? **A:** Transitions are low-priority. React may flush them separately from urgent updates, so they do not always batch together with synchronous state changes. This is expected behavior, not a batching failure. **Q:** In a production app, how would you debug a component that re-renders too many times despite batching? **A:** Open React DevTools Profiler and record the interaction. Look for components with multiple commits in a single user action. Then check for `flushSync` calls, third-party libraries calling `setState` outside React events, or list items missing `key` props that force full subtree reconciliation. The `why-did-you-render` library adds per-component logs that show exactly what prop or state value changed between each render. ## Examples ### Basic: two updates, one render ```jsx function Form() { const [name, setName] = useState(''); const [email, setEmail] = useState(''); const reset = () => { setName(''); // queued setEmail(''); // queued // both cleared in a single render }; return ( <div> <input value={name} onChange={e => setName(e.target.value)} /> <input value={email} onChange={e => setEmail(e.target.value)} /> <button onClick={reset}>Reset</button> </div> ); } ``` One click, two `setState` calls, one render. Without batching the form would flash through an intermediate state where `name` is empty but `email` still holds the old value. ### Intermediate: filter with batched state (TodoMVC pattern) ```jsx function TodoList({ todos }) { const [filter, setFilter] = useState('all'); const [visibleTodos, setVisibleTodos] = useState(todos); const changeFilter = (newFilter) => { setFilter(newFilter); // queued setVisibleTodos( // queued todos.filter(t => newFilter === 'all' || t.status === newFilter ) ); // single render: filter badge and list update together }; return ( <div> <button onClick={() => changeFilter('done')}>Show Done</button> <ul>{visibleTodos.map(t => <li key={t.id}>{t.text}</li>)}</ul> </div> ); } ``` If these two updates rendered separately, you would see a frame where the filter badge says "done" but the list still shows all items. Batching prevents that torn state from ever reaching the screen. ### Advanced: the async trap (React 17 vs 18) I once debugged a dashboard where every data fetch triggered two visible loading flickers. The component was calling `setState` inside a `Promise.then` on React 17. Here is a simplified version: ```jsx function Dashboard() { const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const fetchData = async () => { setLoading(true); // React 17: render #1 const result = await fetch('/api/stats').then(r => r.json()); // Inside Promise.then — React 17 does NOT batch these setData(result); // React 17: render #2 setLoading(false); // React 17: render #3 // React 18: one render after the await }; return ( <div> {loading && <p>Loading...</p>} {data && <pre>{JSON.stringify(data, null, 2)}</pre>} <button onClick={fetchData}>Fetch</button> </div> ); } ``` In React 17 this produces three separate renders, including a frame where `data` is set but `loading` is still `true`. That is the flicker. In React 18, the two post-`await` updates batch automatically. If you are on React 17 and cannot upgrade, wrap the post-await updates in `unstable_batchedUpdates`.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.