Published on

Automatic State Batching in React 18

Authors

The Most Impactful Release Since Version 16.8

When the React component lifecycle was overhauled along with the Context API, followed by React Hooks in versions 16.3 and 16.8 respectively, it had a huge impact on the way we write our React apps. There hasn't been a whole lot to get excited about since then, as version 17 was primarily focused on improving fundamentals and laying the groundwork for the future.

With the upcoming release of version 18 however, there is a lot to be excited about. React 18 will bring us automatic state batching, a new streaming server renderer, and a few juicy new APIs. Let's dig into automatic state batching and see what its all about.

Automatic State Batching

State batching in React is when all of the state updates are combined into a single update, resulting in a single re-render instead of one for each update. In previous versions, updates were only batched for React event handlers, and it wasn't consistent about it (fetching data then updating the state would not batch, for example). With React 18, all updates will be automatically batched, regardless of where they originate from. For example, given a promise like this -

fetch(/*...*/).then(() => {
  setCount(c => c + 1)
  setFlag(f => !f)
})

...React will only re-render one time at the end.

Of course, the biggest advantage of automatic batching across the board certainly comes as a major performance boost, but it also prevents components from rendering unfinished states where only one state variable was updated, potentially resulting in bugs in the UI.

React 18 introduces a new method called createRoot to replace the old render method in the top level component of the app. To take advantage of automatic batching, simply replace the render method in the App component with createRoot after updating the package. You can upgrade to 18 and still use the render method to retain the old functionality without anything breaking, but the upgrade to createRoot is generally expected as part of adopting 18.

There may be certain situations in which you need to avoid automatic batching for certain edge cases. One such edge case is with class components, where it was possible to synchronously read state updates inside of events. In other words, you could read this.state between calls to setState like so:

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

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

    this.setState(({ flag }) => ({ flag: !flag }))
  })
}

With batching in React 18, count will still be 0 when state is logged to the console because React doesn't render the result of the first setState synchronsouly (ie. the render happens on the next browser tick). In this scenario, the ReactDOM.flushSync method can be used to force an update. It is however recommended that it be used sparingly:

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

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

    this.setState(({ flag }) => ({ flag: !flag }))
  })
}

Function components that manage state with the useState hook are unaffected by this because setting state with useState doesn't update the existing state variable.

Further Reading:

[The Plan for React 18] (https://reactjs.org/blog/2021/06/08/the-plan-for-react-18.html) React 18 Discussion Group on Github [Batching in React] (https://crypt.codemancers.com/posts/2021-06-29-batching-in-react/) [Blog: React 18 adds automatic batching] (https://blog.saeloun.com/2021/07/22/react-automatic-batching.html)