Batching in React

Batching in React

What do you think batch means in literal terms? It means to combine, mix or process in a group.

Batching is an essential concept in React. Now in terms of React, what do we mean by batching?

Batching

Batching is the process of grouping multiple state updates to the DOM into a single update.

Preventing the components from re-rendering on each state update can result in significant performance improvements, especially when dealing with complex UIs that require frequent updates

React waits until all code in the event handlers has run before processing the state updates rather than updating it multiple times.

Why do we need Batching?

When you make changes to the state or props of a component, React will usually update the DOM immediately to reflect those changes.

For smaller applications, it may not look like a big deal but for bigger applications, it could cause heavy performance wise since the bigger applications may have many nested components.
And any unbatched state updates in the parent component could cause multiple re-rendering of the entire component tree.

React 17 and prior versions

In React 17 and prior, state updates only inside browser events were batched. But updates inside promises, setTimeout, native event handlers, or any other event were not batched in React by default.
Example of state update associated with the browser events.

import "./styles.css";
import {useState} from "react";

export default function App() {
  const [add, setAdd] =useState(0);
  const [sub, setSub] =useState(0);

  const clickHandler = () =>{
    setAdd(add+1);
    setSub(sub-1);
    console.log("Rendering");
  }

  return (
    <div className="App">
      <p>{add}</p>
      <p>{sub}</p>
      <button onClick={()=>clickHandler()}>Operation</button>
      </div>
  );
}

In the above code, on clicking the button clickHandler() function will be called which will execute 2 state updates.

On observing in the browser console you can see that it logged only one "Rendering" message for both updates. Here re-rendering happened only once for both state updates since react batched both state updates.

In React 17 or prior versions, if we executed the state updates inside setTimeout() or promise or async await the batching would not have happened and in the browser console we would have gotten 2 Rendering messages for 2 state updates.

React 18

React 18 performs Automatic Batching which is an improved version of batching. Its update functionality comes in pre-built throughout the components similarly irrespective of its origin, unlike prior versions.
So in Automatic Batching, batching happens in state updates of asynchronous operations, setTimeout, native event handlers, or any other event.

Now, let's try to update the same state by calling setAdd three times in the same event handler.

import "./styles.css";
import { useState } from "react";

export default function App() {
  const [add, setAdd] = useState(0);

  const clickHandler = () => {
    setAdd(add + 1);
    setAdd(add + 1);
    setAdd(add + 1);
    console.log("Rendering");
  };

  return (
    <div className="App">
      <p>{add}</p>
      <button onClick={() => clickHandler()}>Add (+3)</button>
    </div>
  );
}

So what do you think the value of add will be? Let's hope you got it correct.

The value of add will be 1 and in the browser console, we'll see a single "Rendering" message. Why? It's because of batching.

And the same we'll see in the case of setTimeout or asynchronous operations. No matter how many state updates are happening inside it (3, 5, etc) only one re-rendering will happen for each event.
Which makes React 18 much faster than its prior versions performance-wise.

All things aside what do we do in the above case? We want the state value to be incremented by 3.

We want to update the same state multiple times before the next render.

To achieve this we have to pass the function instead of the next state value. This function calculates the next state based on the previous one in the queue (For example, chaining it like train wagons one after another).

import "./styles.css";
import { useState } from "react";

export default function App() {
  const [add, setAdd] = useState(0);

  const clickHandler = () => {
    setAdd((add) => add + 1);
    setAdd((add) => add + 1);
    setAdd((add) => add + 1);
    console.log("Rendering");
  };

  return (
    <div className="App">
      <p>{add}</p>
      <button onClick={() => clickHandler()}>Add (+3)</button>
    </div>
  );
}

For the above code, the value of add will be 3. And the browser console will print a single "Rendering" message.
Here, (add) => add + 1 is called an updater function. When you pass it to a state setter:

  1. React queues this function to be processed after all the other code in the event handler has run.

  2. During the next render, React goes through the queue and gives you the final updated state.

queued updatecurrent value of addreturns
add => add + 100 + 1 = 1
add => add + 111 + 1 = 2
add => add + 122 + 1 = 3

The updater function should be a pure function.

In Strict Mode, React will run each updater function twice (but discard the second result) to help you find mistakes so always use the pure function to avoid unexpected results.

Some other examples:

const [add, setAdd] = useState(0); 
const clickHandler = () =>
  setAdd(add + 2);
  setAdd(a => a + 1);
  setAdd(100);
}

The output will be for first setAdd(0+2) it returns 2 which will be passed to second setAdd(2 => 2+1) which will return 3 then it will be passed to third setAdd(100) which will return the final result of 100.

The naming of this updater function can be of three types:

(Let's suppose we want to name our updater function above so accordingly)

  1. first letters of the corresponding state variable i.e. a => a + 1

  2. repeating the full state variable name i.e. add => add + 1

  3. to use a prefix i.e. prevAdd => prevAdd + 1

Stopping Automatic Batching

There is indeed a way of stopping Automatic Batching. One can opt out of Batching with the help of ReactDOM.flushSync.

Simply import flushSync from react-dom and then call flushSync method inside the event handler and inside flushSync method's callback place your state update.

import { flushSync } from 'react-dom';

const handleClick = () => {
  flushSync(() => {
    setAdd((add) => add+ 1);
    // react will create a re-render here
  });
  flushSync(() => {
    setSub((sub) => sub- 4);
    // react will create a re-render here
  });
};

Recap:

  • Batching is a functionality of React in which multiple state update calls are grouped into one update, and render the UI only once with the updated data.

  • It's really helpful for bigger applications in terms of performance.

  • React17 or prior, state updates only inside browser events were batched, and promises, setTimeout, native event handlers, or any other event's update were not batched.

  • React18 brought Automatic Batching which will enable batching for all state updates regardless of where they came from.

  • If we want to update the same state multiple times before the next render then use the updater function.

  • The updater function should be pure.

That's all about the Batching in React. Hope I was able to clear this concept :)

References: