Automatic batching for fewer renders in React 18 Automatic batching for fewer renders in React 18

An overview of the

React 18 adds a new optimization feature that allows batching for more scenarios without manual processing in the code. This article explains what batch updating is, how it worked before React 18, and how it has changed since React 18.

What is batch update?

React combines multiple state updates into a single render for better performance.

For example, if there are two status updates in the same click event, React will always batch them into a re-render. If we run the following code, we’ll see that React only performs one render per click, even though the state is set twice:

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

  function handleClick() {
    setCount(c= > c + 1); // Does not re-render yet
    setFlag(f= >! f);// Does not re-render yet
    // React will only re-render once at the end (that's batching!)
  }

  return (
    <div>
      <button onClick={handleClick}>Next</button>
      <h1 style={{ color: flag ? "blue" : "black}} ">{count}</h1>
    </div>
  );
}
Copy the code
  • ✅ Demo: Batch updates to event handlers in React 17 (note that each click is printed in the console)

Batch updates can improve the rendering performance of components because it avoids unnecessary rendering. It also prevents the component from updating only one state variable, causing other state changes in the component not to be fully rendered, which could cause bugs. Similar to ordering food in a restaurant, waiters do not come to the kitchen to place orders as soon as we order our first dish, but wait for the order to be completed before placing an order together.

However, the React batch update will not work for all scenarios. For example, if you request data in handleClick and then update the status after the data request is successful, React does not trigger batch updates, but instead performs two separate updates.

This is because previous versions of React required batch updates to be triggered only during browser events such as click events. However, in the following code example, a status update (in the FETCH callback) does not trigger a batch update when the click event is over long after the data request is successful:

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

  function handleClick() {
    fetchSomething().then(() = > {
      // React 17 and earlier does NOT batch these because
      // they run *after* the event in a callback, not *during* it
      setCount(c= > c + 1); // Causes a re-render
      setFlag(f= >! f);// Causes a re-render
    });
  }

  return (
    <div>
      <button onClick={handleClick}>Next</button>
      <h1 style={{ color: flag ? "blue" : "black}} ">{count}</h1>
    </div>
  );
}
Copy the code
  • 🟡 Demo: React 17 will not batch external event handlers (note that each click will print twice in the console)

Prior to React 18, we only did batch updates during execution in the React event handler. By default, status updates in Promises, setTimeout, native event handlers, or any other event will not be batch updated.

What is automatic batch update?

Starting with createRoot in React 18, all updates are automatically batch updated wherever they are.

This means that batch updates for setTimeout, Promises, native event handlers, or any other event will be batch updated in the same way as React events. We hope this will reduce the rendering effort and thus improve the performance of the application:

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

  function handleClick() {
    fetchSomething().then(() = > {
      // React 18 and later DOES batch these:
      setCount(c= > c + 1);
      setFlag(f= >! f);// React will only re-render once at the end (that's batching!)
    });
  }

  return (
    <div>
      <button onClick={handleClick}>Next</button>
      <h1 style={{ color: flag ? "blue" : "black}} ">{count}</h1>
    </div>
  );
}
Copy the code
  • ✅ Demo: React 18 Uses createRoot to batch process external event handlers (note that each click prints once in the console)
  • 🟡 Demo: React 18 Traditional Render retains the previous behavior (note that each click will print twice in the console)

Note: You are expected to upgrade to React 18 and use createRoot. The old Render was just meant to simplify production experiments for both versions.

React does a batch update wherever the state changes, like this:

function handleClick() {
  setCount(c= > c + 1);
  setFlag(f= >! f);// React will only re-render once at the end (that's batching!)
}
Copy the code

Or something like this:

setTimeout(() = > {
  setCount(c= > c + 1);
  setFlag(f= >! f);// React will only re-render once at the end (that's batching!)
}, 1000);
Copy the code

Or something like this:

fetch(/ *... * /).then(() = > {
  setCount(c= > c + 1);
  setFlag(f= >! f);// React will only re-render once at the end (that's batching!)
})
Copy the code

Or something like this:

elm.addEventListener('click'.() = > {
  setCount(c= > c + 1);
  setFlag(f= >! f);// React will only re-render once at the end (that's batching!)
});
Copy the code

Note: React only performs batch updates when it is normally safe to do so. For example, React needs to ensure that for each user-initiated event (such as a click or keystroke), the DOM is fully updated before the next event. For example, this can prevent forms that are disabled at submission time from being submitted twice.

What if you don’t want to batch update?

Batch processing is generally safe, but some code may rely on reading something from the DOM immediately after a state change. In this case, you can use reactdom.flushsync () to opt out of batch processing:

import { flushSync } from 'react-dom'; // Note: react-dom, not react

function handleClick() {
  flushSync(() = > {
    setCounter(c= > c + 1);
  });
  // React has updated the DOM by now
  flushSync(() = > {
    setFlag(f= >! f); });// React has updated the DOM by now
}
Copy the code

You don’t get to see this very often.

Does this have any effect on Hooks?

If you are using Hooks, bulk updates will “work” in most cases.

How does that affect Classes?

React status updates during event callbacks are always processed in batches, so no changes are made to these updates.

There is an extreme case in class components that can cause problems.

The class component has a special concern that it can synchronously read status updates within events. That is, it is possible to read this.state between calls to setState:

handleClick = () = > {
  setTimeout(() = > {
    this.setState(({ count }) = > ({ count: count + 1 }));

    // { count: 1, flag: false }
    console.log(this.state);

    this.setState(({ flag }) = > ({ flag: !flag }));
  });
};
Copy the code

In React 18, the above phenomenon will not occur. Because all state updates in setTimeout are batch processed, React will not render the results of the first setState synchronously — rendering will happen in the next browser cycle. So rendering hasn’t happened yet:

handleClick = () = > {
  setTimeout(() = > {
    this.setState(({ count }) = > ({ count: count + 1 }));

    // { count: 0, flag: false }
    console.log(this.state);

    this.setState(({ flag }) = > ({ flag: !flag }));
  });
};
Copy the code

See the sandbox

If this problem prevents upgrading to React 18, you can use reactdom.flushsync to force the update, but caution is advised:

handleClick = () = > {
  setTimeout(() = > {
    ReactDOM.flushSync(() = > {
      this.setState(({ count }) = > ({ count: count + 1 }));
    });

    // { count: 1, flag: false }
    console.log(this.state);

    this.setState(({ flag }) = > ({ flag: !flag }));
  });
};
Copy the code

See the sandbox

This issue does not affect the Hooks function component because setting state does not update existing variables in useState:

function handleClick() {
  setTimeout(() = > {
    console.log(count); / / 0
    setCount(c= > c + 1);
    setCount(c= > c + 1);
    setCount(c= > c + 1);
    console.log(count); / / 0
  }, 1000)
Copy the code

In the component that uses the Hooks function, nothing is done and it has paved the way for batch updates.

unstable_batchedUpdatesWhat is?

Some React libraries use this API to force a batch update of setState outside of event handling:

import { unstable_batchedUpdates } from 'react-dom';

unstable_batchedUpdates(() = > {
  setCount(c= > c + 1);
  setFlag(f= >! f); });Copy the code

This API still exists in 18, but it is no longer needed because batch processing is automated. We did not remove it in version 18, but it will be removed in a future release when mainstream libraries no longer rely on it.

React batch updates can only be triggered automatically for certain scenarios (status updates during event callbacks). For other scenarios, unstable_batchedUpdates can only be used for batch updates. In version 18, the class component no longer supports synchronous status updates in certain scenarios. You need to call flushSync to update the status. FlushSync shuts batch updates.


Search “ikoofe” on wechat, follow the public account “KooFE front-end team”, and release front-end technology articles from time to time.