React Automatic Batching

Understanding how React groups state updates for better performance

⚡ What is Automatic Batching?

Automatic batching is React's way of grouping multiple state updates into a single re-render for better performance. React 18 automatically batches updates everywhere, including promises, timeouts, and event handlers.


function App() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  function handleClick() {
    setCount(c => c + 1); // Doesn't re-render yet
    setFlag(f => !f);     // Batched together - one re-render
  }
}
                                    

Result:

Both state updates trigger only ONE re-render instead of two.

Batching Concepts

🎯

Event Handlers

Batches updates in click handlers

onClick={() => {
  setState1(x);
  setState2(y);
  // One render
}}
⏱️

Async Functions

Batches in promises & timeouts

setTimeout(() => {
  setState1(x);
  setState2(y);
  // One render (React 18+)
}, 1000);
🔄

Native Events

Batches in native event listeners

element.addEventListener('click', () => {
  setState1(x);
  setState2(y);
  // One render
});
🚀

Performance

Reduces unnecessary re-renders

// Multiple updates
// = Single render
// = Better performance

🔹 Batching in Event Handlers

React automatically batches state updates inside event handlers. Multiple setState calls result in only one re-render, improving performance significantly.

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  console.log('Component rendered');

  function handleClick() {
    // Both updates are batched together
    setCount(count + 1);
    setFlag(!flag);
    // Only ONE re-render happens here
  }

  return (
    <div>
      <p>Count: {count}</p>
      <p>Flag: {flag.toString()}</p>
      <button onClick={handleClick}>Update Both</button>
    </div>
  );
}

Output:

Console shows "Component rendered" only ONCE per click, not twice.

🔹 Batching in Async Code (React 18+)

React 18 introduced automatic batching everywhere, including promises, setTimeout, and native event handlers. Previously, these required manual batching with ReactDOM.unstable_batchedUpdates().

import React, { useState } from 'react';

function AsyncBatching() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  console.log('Rendered');

  function handleClick() {
    // Fetch data from API
    fetch('/api/data').then(() => {
      // React 18: These are batched automatically
      setCount(c => c + 1);
      setFlag(f => !f);
      // Only ONE re-render
    });

    // Also works with setTimeout
    setTimeout(() => {
      setCount(c => c + 1);
      setFlag(f => !f);
      // Only ONE re-render
    }, 1000);
  }

  return (
    <div>
      <p>Count: {count}</p>
      <p>Flag: {flag.toString()}</p>
      <button onClick={handleClick}>Async Update</button>
    </div>
  );
}

Output:

In React 18+, async updates are batched. In React 17, they would cause separate re-renders.

🔹 Opting Out of Batching

Sometimes you need immediate updates without batching. Use flushSync() to force synchronous updates, though this should be rare as it reduces performance.

import React, { useState } from 'react';
import { flushSync } from 'react-dom';

function OptOutBatching() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  function handleClick() {
    // Force immediate update (not batched)
    flushSync(() => {
      setCount(c => c + 1);
    });
    // Component re-renders here

    // Another immediate update
    flushSync(() => {
      setFlag(f => !f);
    });
    // Component re-renders again
    
    // Total: TWO re-renders instead of one
  }

  return (
    <div>
      <p>Count: {count}</p>
      <p>Flag: {flag.toString()}</p>
      <button onClick={handleClick}>Force Sync Updates</button>
    </div>
  );
}

Output:

⚠️ Use flushSync() sparingly - it bypasses batching and reduces performance.

🔹 React 17 vs React 18 Batching

Understanding the difference between React versions helps when upgrading or debugging batching behavior in your applications.

React 17 (Old Behavior):

  • ✅ Batched in React event handlers
  • ❌ NOT batched in promises
  • ❌ NOT batched in setTimeout
  • ❌ NOT batched in native event listeners

React 18 (New Behavior):

  • ✅ Batched in React event handlers
  • ✅ Batched in promises
  • ✅ Batched in setTimeout
  • ✅ Batched in native event listeners
  • ✅ Batched everywhere automatically!
// React 17: Two re-renders
setTimeout(() => {
  setCount(c => c + 1); // Re-render 1
  setFlag(f => !f);     // Re-render 2
}, 1000);

// React 18: One re-render
setTimeout(() => {
  setCount(c => c + 1); // Batched
  setFlag(f => !f);     // Batched
}, 1000); // Only one re-render!

🔹 Best Practices

Follow these guidelines to make the most of automatic batching:

  • Upgrade to React 18+ for automatic batching everywhere
  • Use functional updates when new state depends on old state
  • Avoid flushSync() unless absolutely necessary
  • Trust automatic batching - React handles it for you
  • Don't manually optimize what React already optimizes
// ✅ Good: Functional updates
setCount(c => c + 1);
setFlag(f => !f);

// ❌ Avoid: Direct state reference (can be stale)
setCount(count + 1);
setFlag(!flag);

🧠 Test Your Knowledge

In React 18, are state updates in setTimeout automatically batched?