How React Batches State Updates (And When It Doesn’t)

Ravali Peddi
3 min readFeb 25, 2025

--

Image credit — undraw

Understanding React’s State Batching Mechanism

State batching is an optimization in React that reduces the number of re-renders by grouping multiple state updates into a single render cycle. React 18 introduced automatic batching, which significantly enhances performance. However, there are scenarios where React doesn’t batch updates, and understanding these cases is crucial for writing efficient React applications.

1. What is State Batching?

State batching allows React to group multiple state updates together and commit them in one render pass. This avoids unnecessary re-renders and improves performance.

Example of Batching in React 17 and Earlier

Prior to React 18, batching was only done inside event handlers:

import { useState } from 'react';

const Counter = () => {
const [count, setCount] = useState(0);
const [text, setText] = useState('Initial');
const handleClick = () => {
setCount((c) => c + 1);
setText('Updated');
// Only one re-render occurs because React batches state updates in event handlers
};
return <button onClick={handleClick}>{count} - {text}</button>;
};

However, outside of event handlers (e.g., inside setTimeout, Promises, or native event listeners), state updates were not batched:

setTimeout(() => {
setCount((c) => c + 1);
setText('Updated');
// Two re-renders occur in React 17
}, 1000);

2. React 18’s Automatic Batching

React 18 expands batching to cover all asynchronous updates (timeouts, promises, async functions, native event listeners, etc.).

Example: Batching in Asynchronous Code in React 18

import { useState } from 'react';

const App = () => {
const [count, setCount] = useState(0);
const [text, setText] = useState('Initial');
const handleAsyncUpdate = () => {
setTimeout(() => {
setCount((c) => c + 1);
setText('Updated');
// Single re-render in React 18 (batched)
}, 1000);
};
return <button onClick={handleAsyncUpdate}>{count} - {text}</button>;
};

In React 18, these updates are now batched, preventing unnecessary re-renders.

3. When React Does NOT Batch Updates

Despite the improvements, React still doesn’t batch in some cases:

(a) Updates inside await calls

React stops batching once an await is encountered in an async function:

const handleAsync = async () => {
setCount((c) => c + 1);
await new Promise((resolve) => setTimeout(resolve, 1000));
setText('Updated'); // This triggers a separate re-render
};

Because React cannot track execution across await, the second update happens in a new render cycle.

(b) Manually Forcing a Re-render

Calling flushSync from react-dom forces updates to happen immediately, bypassing batching:

import { flushSync } from 'react-dom';

const handleImmediateUpdate = () => {
flushSync(() => setCount((c) => c + 1));
flushSync(() => setText('Updated'));
// Two separate renders occur
};

Use flushSync only when necessary, as it can degrade performance if overused.

4. Common Pitfalls and Misconceptions

1. Assuming useState Updates Are Always Synchronous

A common mistake is assuming that state updates are applied immediately. React schedules updates and batches them efficiently, meaning state might not reflect changes right away:

const [count, setCount] = useState(0);

const handleClick = () => {
setCount(count + 1);
console.log(count); // Might log 0, not 1, due to batching
};

To always get the latest state, use the function form of setState:

setCount((prev) => prev + 1);

2. Using flushSync Incorrectly

Some developers misuse flushSync to ensure immediate state updates. However, doing so unnecessarily can hurt performance by forcing React to flush updates synchronously:

const handleClick = () => {
flushSync(() => setCount((c) => c + 1));
flushSync(() => setText('Updated'));
// Avoid this unless absolutely necessary
};

Instead, rely on batching unless there’s a strict UI requirement for immediate updates.

5. Summary & Best Practices

  • React batches state updates by default in event handlers and async operations (React 18+).
  • React does NOT batch updates across await calls.
  • Use flushSync cautiously when immediate updates are required.
  • Leverage batching to minimize unnecessary re-renders and improve performance.
  • Be aware of state updates being asynchronous and use functional updates when necessary.

Understanding these nuances helps in writing optimized React applications with predictable rendering behavior.

Sign up to discover human stories that deepen your understanding of the world.

--

--

Ravali Peddi
Ravali Peddi

No responses yet

Write a response