← Back to Chapters

React 18 State Batching

⚛️ React 18 State Batching

? Quick Overview

State batching is how React groups multiple state updates together and performs a single re-render instead of one render per update. This improves performance and keeps the UI efficient.

In React 18, automatic batching was expanded — it now works not only in React event handlers (like onClick), but also in async functions, promises, timeouts, and more.

⚡ Key idea: Multiple state updates → one render

? Key Concepts

  • Batching: React groups several state updates into one render pass.
  • Automatic in React 18: Works in events, async calls, promises, and timeouts.
  • Functional updates: Using setState(prev => prev + 1) is safest when batching.
  • flushSync (advanced): Lets you opt out of batching and force an immediate render.

? Syntax & Theory

React collects multiple state updates that happen in the same event loop tick and applies them together. Even if you call several setters one after another, React schedules a single re-render.

When batching, you should prefer functional updates so that each update works with the latest value:

? View Code Example
// Use functional updates to safely batch multiple increments
const [count, setCount] = React.useState(0);
function handleClick() {
setCount((c) => c + 1);
setCount((c) => c + 1);
setCount((c) => c + 1);
// After batching, count increases by 3 in a single render
}

Without functional updates, each call might read a stale value of count, especially when multiple updates are batched together.

? Example 1: Batching in a Click Handler

When you update multiple pieces of state in a single event handler, React batches them and re-renders once.

? View Code Example
// React 18 automatically batches multiple state updates in one event
function Counter() {
const [count, setCount] = React.useState(0);
const [message, setMessage] = React.useState("");
const handleClick = () => {
setCount((c) => c + 1);
setMessage("Updated!");
// Both state updates above cause a single re-render
};
console.log("Rendered");
return (
<div>
<p>Count: {count}</p>
<p>{message}</p>
<button onClick={handleClick}>Click Me</button>
</div>
);
}

Even though setCount and setMessage are called separately, the component logs "Rendered" only once per click, because React batches the updates.

? Example 2: React 18 Automatic Batching in Async Code

Before React 18, batching worked only in React event handlers. In async code (like fetch() or setTimeout()), each state update caused its own render. React 18 fixes this.

? View Code Example
// React 18 batches state updates even after async work
function Example() {
const [count, setCount] = React.useState(0);
const [loading, setLoading] = React.useState(false);
const fetchData = async () => {
setLoading(true);
// Simulate network delay
await new Promise((r) => setTimeout(r, 1000));
setCount((c) => c + 1);
setLoading(false);
// All updates above are batched into a single re-render
};
console.log("Rendered");
return (
<div>
<p>Count: {count}</p>
<p>Loading: {loading ? "Yes" : "No"}</p>
<button onClick={fetchData}>Fetch Data</button>
</div>
);
}

Both setLoading and setCount are applied together: React does a single re-render after the async work completes, keeping the UI consistent and efficient.

? Before React 18 (No Async Batching)

In older versions of React, only updates inside React event handlers were batched. Async updates caused multiple renders.

? View Code Example
// Before React 18, async updates like this caused multiple renders
async function updateValues() {
setCount(count + 1);   // Triggers first re-render
await fetch("/api/data");
setLoading(false);     // Triggers second re-render
}

With React 18’s automatic batching, both updates can be grouped, avoiding extra renders and improving performance.

? Disabling Batching with flushSync (Advanced)

In rare cases, you may need React to flush updates immediately, without batching. You can use flushSync from react-dom to do this.

? View Code Example
// Use flushSync only when you really need an immediate render
import { flushSync } from "react-dom";
function Example() {
const [count, setCount] = React.useState(0);
const handleClick = () => {
flushSync(() => setCount((c) => c + 1));
// Forces React to flush this update right away
console.log("Flushed render before continuing");
};
return <button onClick={handleClick}>Click</button>;
}

Use flushSync sparingly — mainly for critical UI updates (e.g., certain animations or syncing with external systems) where you cannot wait for the normal batched render.

? Live Output / Explanation

What you would observe in the browser

  • In the Counter example, clicking the button:
    • Increases count by 1.
    • Sets message to "Updated!".
    • Logs "Rendered" only once per click.
  • In the async example:
    • Clicking Fetch Data sets loading to "Yes".
    • After the delay, count increments and loading becomes "No".
    • All state changes are applied in one render after the async work completes.
  • With flushSync, the UI updates immediately at the point where flushSync is called, before any later code in the handler runs.

? Benefits of Automatic Batching

  • ✅ Reduces unnecessary re-renders → faster performance.
  • ✅ Works automatically in most scenarios — no extra setup.
  • ✅ Handles async updates (fetch, timeouts, promises) more efficiently.
  • ✅ Keeps the UI consistent while multiple state values change together.

? Tips & Best Practices

  • React batches all state updates inside the same event loop tick.
  • Prefer functional updates (prev => prev + 1) when batching multiple updates.
  • Do not rely on state being updated immediately after calling setState.
  • Use flushSync only for rare, critical UI updates where timing is crucial.

? Try It Yourself

  1. Create a component that updates two states (count and message) in the same function and observe that React still does a single render.
  2. Add setTimeout() or an async fetch() call and confirm that React 18 automatically batches updates after the async work.
  3. Log "Rendered" in your component body to visually confirm how often React re-renders.
  4. Experiment with flushSync() to force an immediate render and compare it with the batched behavior.

Goal: Understand how React 18 automatically batches multiple (even asynchronous) state updates to optimize rendering and improve performance in real-world components.