Batching in React

Written by Sujay Prabhu on June 29, 2021; tagged under reactjs

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

If you have any questions or feedback, feel free to drop us a mail at team@codemancers.com.