DEV Community

Cover image for How React Batches State Updates (And When It Doesn’t)
Ravali Peddi
Ravali Peddi

Posted on

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

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>;
};
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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>;
};
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

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

setCount((prev) => prev + 1);
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (1)

Collapse
 
ravi-coding profile image
Ravindra Kumar

Good !