Improve app performance using automatic batching with React 18

Cover Image for Improve app performance using automatic batching with React 18

React 18 is COMING!!!

Excited gif

React core team released the Alpha version of React 18 recently and it includes out-of-the-box improvements to existing features like:

  • Automatic batching for fewer renders
  • SSR support for Suspense
  • Fixes for Suspense behavior quirks

In this blog, we'll be talking about what batching is, how it previously worked, and what has changed.

What is Batching?

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

Let's see this with the help of an example:
Suppose, we have two state updates inside of an event click handler. React will batch these two updates and render only once.

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

  const clickHandler = () => {
    setCount((count) => count + 1); // Does not re-render here
    setToggle((toggle) => !toggle); // Does not re-render here
        // React will only re-render once at the end(and that's batching)
  };

  console.log("Rendered ", count, toggle);

  return (
    <div>
      <button onClick={clickHandler}>Click Me</button>
      <h1>Count: {count}</h1>
      <h1>Toggle: {toggle.toString()}</h1>
    </div>
  );
}

In the above example, the view will render only once after the button has been clicked.

auto batching react 17.JPG

React does automatic batching for us in the case of event handlers. It is smart enough to batch two updates into one resulting in only one render and hence improving the performance of the app.

💻 Demo: Batch updates inside event handlers in React 17

The problem with batching

React is doing automatic batching for us, improving the app performance and everything is going well.

BUT, there is one problem. React isn't consistent with the batch updates.

Let's see an example:
Suppose, we need to fetch data or use setTimeout and then update the state according to the result. Well, in this case, React wouldn't batch the updates and perform two independent state updates hence triggering two renders.

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

  const clickHandler = () => {
    setTimeout(() => {
      setCount((count) => count + 1);
      setToggle((toggle) => !toggle);
    }, 100);
  };

  console.log("Rendered ", count, toggle);

  return (
    <div>
      <button onClick={clickHandler}>Click Me</button>
      <h1>Count: {count}</h1>
      <h1>Toggle: {toggle.toString()}</h1>
    </div>
  );
}

In the above example, the app will re-render after setCount and then again after setToggle resulting in two renders.

problem with batching react 17.JPG

💻 Demo: Updates outside event handlers are NOT batched in React 17

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.

The Solution

React core team came up with the idea of automatic batching for React 18 meaning that now all updates will be automatically batched, irrespective of their origin. So now the updates inside the timeouts or network calls would be handled the same way as events.

Note: It is expected that you have installed React 18 Alpha and enabled Concurrent Mode.

Let's take the same example and try it with React 18:

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

  const clickHandler = () => {
    setTimeout(() => {
      setCount((count) => count + 1);
      setToggle((toggle) => !toggle);
    }, 100);
  };

  console.log("Rendered ", count, toggle);

  return (
    <div>
      <button onClick={clickHandler}>Click Me</button>
      <h1>Count: {count}</h1>
      <h1>Toggle: {toggle.toString()}</h1>
    </div>
  );
}

Now, with React 18, both the updates with be automatically batched and the app will render only once.

auto batching react 18.JPG

💻 Demo: Batch updates outside event handlers in React 18

But wait, what if you don't want to batch?

Shocked gif

There can be times where you wouldn't want to automatically batch updates.

Well, flushSync comes to the rescue.

We can use ReactDOM.flushSync() to opt-out of batching.

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

  const clickHandler = () => {
    flushSync(() => {
      setCount((count) => count + 1);
    });
    // React has updated the DOM by now
    flushSync(() => {
      setToggle((toggle) => !toggle);
    });
    // React has updated the DOM by now
  };

  console.log("Rendered ", count, toggle);

  return (
    <div>
      <button onClick={clickHandler}>Click Me</button>
      <h1>Count: {count}</h1>
      <h1>Toggle: {toggle.toString()}</h1>
    </div>
  );
}

In the above code, we have used flushSync() to update setCount and setToggle independently resulting in two renders.

flushSync.JPG

💻 Demo: Using flushSync to update states independently in React 18

TL;DR

Until React 18, React only batched updates inside event handlers. Any other updates from promises, setTimeout or any other event were not batched.

Starting from React 18, react will batch all updates automatically, irrespective of their origin.

If you wish to opt-out of batching in React 18, you can achieve this by using flushSync()



Thank you for reading this blog. This is my first ever blog so I'm pretty nervous about putting this out.

Please feel free to provide any feedback or suggestions in the comments below. If you liked the blog and found it useful, do share it. 😀