How batched state updates differ in React 17 vs React 18

React has consistently offered a certain level of performance optimisation when handling state updates by batching them. Typically, state updates trigger a UI re-render. Therefore, to optimise performance, React batches some of these state updates together, ensuring that only one re-render occurs for all of them, rather than one re-render for each individual update. The key point in last sentence centers around the phrase ‘some of these updates.’ In this blog post, I aim to highlight the scenarios where batching takes place and explore the differences between React 17 and React 18.

According to the official documentation, before React 18, state updates were batched only for React event handlers (i.e., synthetic events and not for native events). However, I wanted to try it out and see how this actually works. Below, I will go through some scenarios to clarify the understanding.

Scenarios

Here are the links for working examples for React 17 and React 18 (check the render count in console) . I’ll try out different scenarios.

Initial render (without useEffect)

No of renders
React 171
React 181

Since we don’t have any useEffect, the component gets rendered once when it mounts in both cases.

Initial render (with useEffect)

In this case, we set the values of A and B in useEffect by making two setState calls.

  useEffect(() => {
    setValA((p) => p + "A");
    setValB((p) => p + "B");
   }, []);
No of renders
React 172
React 182

What we observe now is that in both cases there are 2 renders i.e. one initial render and another one due to setter call in useEffect but the important point is that for useEffect there is only one re-render and not two as the state updates are batched together and UI is re-rendered only once. This behaviour is same in both React 17 and 18.

Initial render (with useEffect) and setting state asynchronously

In this scenario, we will make one change by performing the state updates within a setTimeout.

  useEffect(() => {
    setTimeout(() => {
      setValA((p) => p + "A");
      setValB((p) => p + "B");
    }, 500);
   }, []);
No of renders
React 173
React 182

In this case, the behavior diverges because React 17 does not batch state updates inside setTimeout. This is where React 18 has introduced the concept of Automatic batching, which expands the scope of batching and works in almost all cases.

Let’s explore some other scenarios where automatic batching brings improvements to the rendering process.

Event handlers performing state updates synchronously

In this case, we will update the values of two lists in a click event handler and observe the number of re-renders that occur. For simplicity, we will omit the use of useEffect in this and all subsequent examples.

  function handleClick() {
    setValA((p) => p + "A");
    setValB((p) => p + "B");
  }
No of renders (after click)
React 171
React 181

Once again, we observe that both React 17 and 18 behave in the same manner, where updates in the click event handler are batched together, resulting in a single re-render.

Event handlers performing state updates asynchronously

 function handleClick() {
    setTimeout(() => {
      setValA((p) => p + "A");
      setValB((p) => p + "B");
    }, 500);
  }
No of renders (after click)
React 172
React 181

Again as expected the behaviour differs due to automatic batching taking place in react 18.

Event handler with async calls

Let’s examine how rendering behaves when we make an asynchronous call in the event handler, such as a server fetch API call, and update the state with the results of the call.

  async function handleClick() {
    const valuesA = await asyncFetchA();
    const valuesB = await asyncFetchB();
    setListA(valuesA);
    setListB(valuesB);
  }
No of renders (after click)
React 172
React 181

Once again, automatic batching in React 18 results in only one render, as opposed to two in React 17. The same scenario with a synchronous call shows no difference, as React 17 also batches the updates.

Conclusion

This illustrates how automatic batching optimises the rendering process in React, potentially providing a performance boost for complex components with minimal additional effort.

One important point to note is that when using React 18, the new architecture and automatic batching are applied only when we use the new createRoot API from ReactDOM.

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <DataList />

Another interesting point to note in the event handler example above is that if we simply change the sequence of setState calls as shown below, you’ll observe that even in React 18, there are two renders.

  async function handleClick() {
    const valuesA = await asyncFetchA();
    setListA(valuesA);
    const valuesB = await asyncFetchB();
    setListB(valuesB);
  }

This is because of how event loop works in javascript and is a totally different topic. If you are interested in knowing more do checkout this and this talk.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.