Batching in React

by Sujay Prabhu, Senior System Analyst

React 18 is coming up with Automatic Batching for fewer renders along with new other features like SSR support for Suspense. Support for concurrent features will help in improving user experience. Batching was present in React from the previous versions as well. But, with the introduction of Automatic Batching, there will be uniformity in the behaviour of re-rendering.

In React, components re-render if the value of state or props is altered. State updates can be scheduled using setState in case of Class components. In Functional components, state values can be updated using the function returned by useState.

React performs Batching while updating component state for performance improvement. Batching means grouping multiple state updates into a single re-render. Let us see how Batching used to work before v18 and what changes have been brought in v18.

Batching before React 18

React, by default performs batch updates only inside event-handlers.

So, setState is asynchronous only inside event handlers. But, synchronous inside of async functions like Promises, setTimeout

class App extends React.Component {
  constructor() {
    super();
    this.state = {
      name: 'setState',
    };
    this.handleClickSync = this.handleClickSync.bind(this);
    this.handleClickAsync = this.handleClickAsync.bind(this);
  }

  handleClickSync() {
    Promise.resolve().then(() => {
      this.setState({ name: 'sync' });
      console.log('state', this.state.name); // sync
    });
  }

  handleClickAsync() {
    this.setState({ name: 'Async' });
    console.log('state', this.state.name); // sync (value of previous state)
    // after re-render state value will be `Async`
  }

  render() {
    return (
      <div>
        <h1 onClick={this.handleClickSync}>Sync setState</h1>
        <h1 onClick={this.handleClickAsync}>Async setState</h1>
      </div>
    );
  }
}

If n state updates were present in async functions, React re-renders component n number of times, updating one state per render.

const App = () => {
  const [counter1, setCounter1] = useState(0);
  const [counter2, setCounter2] = useState(0);

  const handleClickWithBatching = () => {
    setCounter1((count) => count + 1);
    setCounter2((count) => count + 1);
  };

  const handleClickWithoutBatching = () => {
    Promise.resolve().then(() => {
      setCounter1((count) => count + 1);
      setCounter2((count) => count + 1);
    });
  };

  console.log('counters', counter1, counter2);
  /* 
    On click of Single re-render
    conuters: 1 1
   
    On click of Multiple re-render
    conuters: 2 1
    counters: 2 2
   */

  return (
    <div className="App">
      <h2 onClick={handleClickWithBatching}>Single Re-render</h2>
      <h2 onClick={handleClickWithoutBatching}>Multiple Re-render</h2>
    </div>
  );
};

However, forced batching can be implemented with the help of an unstable API ReactDOM.unstable_batchedUpdates

import { unstable_batchedUpdates } from 'react-dom';

const handleClickWithoutBatching = () => {
  Promise.resolve().then(() => {
    unstable_batchedUpdates(() => {
      setCounter1((count) => count + 1);
      setCounter2((count) => count + 1);
    });
  });
};

/* 
 On click of Single re-render
 conuters: 1 1
   
 On click of Multiple re-render
 counters: 2 2
*/
Note: The API is unstable in the sense that React might remove this API once uniformity is brought in the functionality of Batching

Batching in React 18

React 18 performs automatic batching with the help of createRoot API.

ReactDOM.createRoot(document.getElementById('root')).render(<App />);

setState is asynchronous even inside async fucntions.

Batching will be done throughout the components similarly irrespective of its origin.

const handleClick = () => {
  setCounter1((count) => count + 1);
  setCounter2((count) => count + 1);
};

const handleClick = () => {
  Promise.resolve().then(() => {
    setCounter1((count) => count + 1);
    setCounter2((count) => count + 1);
  });
};

//In both cases, component will be re-rendered only once

One can opt out of Batching with the help of ReactDOM.flushSync

import { flushSync } from 'react-dom';

const handleClick = () => {
  flushSync(() => {
    setCounter1((count) => count + 1);
  });
  flushSync(() => {
    setCounter2((count) => count + 1);
  });
};

ReactDOM.unstable_batchedUpdates API still exists in React 18, but it might be removed in the coming major versions.

Happy Learning

More articles

Operating Kafka in Rails with Karafka: Production Architecture, Consumers, and DLQs (Part 2)

In Part 2, we dive deep into the Sync-Out pipeline—how Rails publishes events to Kafka, how our legacy adapter writes to SQL Server 2009 using TinyTDS, and how Dead-Letter Queues (DLQs) became our lifeline during production incidents. This post covers transaction management, service objects, and operational workflows for handling failures.

Read more

Operating Kafka in Rails with Karafka: Production Architecture, Consumers, and DLQs (Part 1)

This post breaks down our production architecture for event streaming in Rails using Kafka and Karafka—from designing producers and consumer flows to handling failures with DLQs and keeping warehouse databases in sync reliably.

Read more

Ready to Build Something Amazing?

Codemancers can bring your vision to life and help you achieve your goals