Original link: github.com/reactwg/rea…

Automatic batching for fewer renders in React 18

React 18 adds out-of-the-box performance improvements by doing more batching by default, removing the need to manually batch updates in application or library code. This post will explain what batching is, how it previously worked, and what has changed.

React 18 does automatic batching for fewer renders

React 18 adds out-of-the-box performance improvements by doing more batching by default, eliminating the need for manual batching updates in application or library code. This article will explain what batch processing is, how it used to work, and what has changed.

Note: this is an in-depth feature that we don’t expect most users to need to think about. However, it may be relevant to educators and library developers.

Note: This is an in-depth feature that we don’t want most users to think about. However, it may be of interest to educators and library developers.

What is batching?

Batching is when React groups multiple state updates into a single re-render for better performance.

For example, if you have two state updates inside of the same click event, React has always batched these into one re-render. If you run the following code, you’ll see that every time you click, React only performs a single render although you set the state twice:

What is batch processing?

Batch processing refers to React grouping multiple state updates into a single rerender for better performance. For example, if you have two status updates in the same click event, React will always batch those updates into a re-render. If you run the following code, you’ll see that React only performs one render per click, even though you set the state 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: React 17 batches inside event handlers. (Notice one render per click in the console.)

This is great for performance because it avoids unnecessary re-renders. It also prevents your component from rendering “half-finished” states where only one state variable was updated, which may cause bugs. This might remind you of how a restaurant waiter doesn’t run to the kitchen when you choose the first dish, but waits for you to finish your order.

This is great for performance because it avoids unnecessary re-rendering. It also prevents components from rendering semi-finished state with only one state variable updated, which can cause bugs. This might remind you that when you order your first course, the restaurant staff will not immediately bring it to the kitchen, but wait for you to finish.

However, React wasn’t consistent about when it batches updates. For example, if you need to fetch data, and then update the state in the handleClick above, then React would

not

batch the updates, and perform two independent updates.

However, React is inconsistent in the timing of batch updates. For example, if you need to fetch data and update the state in handleClick above, React will not update in bulk, but will perform two separate updates.

This is because React used to only batch updates

during

a browser event (like click), but here we’re updating the state

after

the event has already been handled (in fetch callback):

This is because React usually only does bulk updates in browser events (such as clicks), but here we update the state after the event has been processed (in the fetch callback function).

function 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 does NOT batch outside event handlers. (Notice two renders per click in the console.)

Until React 18, we only batched updates during the React event handlers. Updates inside of promises, setTimeout, native event handlers, or any other event were not batched in React by default.

Prior to React 18, we only made batch updates in the React event handler. By default, updates in Promises, setTimeout, native event handlers, or any other event are not batch updated in React.

What is automatic batching?

Starting in React 18 with createRoot, all updates will be automatically batched, no matter where they originate from.

This means that updates inside of timeouts, promises, native event handlers or any other event will batch the same way as updates inside of React events. We expect this to result in less work rendering, and therefore better performance in your applications:

What is automatic batching?

Starting with createRoot in React 18, all updates are automatically batched, no matter where they come from.

This means that updates in Timer, Promises, native event handlers, or any other event will be processed in the same batch as updates in 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 withcreateRootbatches even outside event handlers! (Notice one render per click in the console!)
  • 🟡 Demo: React 18 with legacyrenderkeeps the old behavior (Notice two renders per click in the console.)

Note: It is expected that you will upgrade tocreateRoot as part of adopting React 18. The old behavior with render only exists to make it easier to do production experiments with both versions. React will batch updates automatically, no matter where the updates happen, so this:

Note: While using React 18, you upgrade to Createroot. The old render behavior exists just to make it easier to do both versions of the production experiment.

React will automatically batch update no matter where the update takes place

// compositing events

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

behaves the same as this:

// timer

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

behaves the same as this:

// The callback function

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

behaves the same as this:

// In the native event

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 batches updates when it’s generally safe to do. For example, React ensures that for each user-initiated event like a click or a keypress, the DOM is fully updated before the next event. This ensures, for example, that a form that disables on submit can’t be submitted twice.

Note: React only does batch updates when it is normally safe.

React, for example, ensures that every user action, such as a click or keystroke, the DOM is fully updated before the next event.

This ensures, for example, that a form that is disabled at submission time cannot be submitted twice.

What if I don’t want to batch?

Usually, batching is safe, but some code may depend on reading something from the DOM immediately after a state change. For those use cases, you can use ReactDOM.flushSync() to opt out of batching:

What if I don’t want batch processing?

In general, batch processing is safe, but some code may rely on reading something from the DOM immediately after a state change.

For these use cases, you can choose not to batch using reactdom.flushsync ()

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

We don’t expect this to be common.

We don’t think this is a universal phenomenon.

Does this break anything for Hooks? If you ‘re using Hooks, we expect automatic batching to “just work” in the vast majority of cases. (Tell us if it doesn’t!)

Is this a destructive update to Hooks?

If you are using Hooks, we expect automatic batching to “work” in the vast majority of cases. (If not, let us know!)

Does this break anything for Classes?

Keep in mind that updates

during

React event handlers have always been batched, so for those updates there are no changes.

There is an edge cases in class components where this can be an issue.

Class components had an implementation quirk where it was possible to synchronously read state updates inside of events. This means you would be able to read this.state between the calls to setState:

Does this have any effect on Class?

Keep in mind that updates during React event processing are always batch, so nothing changes with these updates.

This can be a problem in the edge case of class components. Class components have an implementation quirk that allows synchronous reading of status updates within events. That means you can read this. Between setState calls

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, this is no longer the case. Since all of the updates even in setTimeout are batched, React doesn’t render the result of the first setState synchronously — the render occurs during the next browser tick.so The render hasn’t happened yet:

In React 18, that’s no longer the case. Because all updates in setTimeout are batch, React does not render the results of the first setState synchronously, rendering takes place 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 sandbox.

If this is a blocker to upgrading to React 18, you can use ReactDOM.flushSync to force an update, but we recommend using this sparingly:

If this blocks the React 18 upgrade, you can use reactdom.flushsync to force the update, but we recommend caution

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 sandbox.

This issue doesn’t affect function components with Hooks because setting state doesn’t update the existing variable from useState:

This issue does not affect function components that use Hooks because setting state does not update existing variables from 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

While this behavior may have been surprising when you adopted Hooks, it paved the way for automated batching.

This behavior may be surprising when you adopt hooks, but it paves the way for automated batching.

What about unstable_batchedUpdates?

Some React libraries use this undocumented API to force setState outside of event handlers to be batched:

Unstable_batchedUpdates?

Some React libraries use this undocumented API to force setState outside the event handler to be batched:

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 isn’t necessary anymore because batching happens automatically. We are not removing it in 18, although it might get removed in a future major version after popular libraries no longer depend on its existence.

This API still exists in 18, but is no longer necessary because batch processing occurs automatically. We won’t remove it in 18, although it may be removed in future major releases, since popular libraries no longer depend on its existence.