React Batched Updates

How to combine state updates in a single render call

React.js batched updates

You already know that whenever you update the state of a component, React’s diffing algorithm will detect changes in the virtual DOM and render your component in the real DOM. But does this really happen for every single state-update? Or does React provide a way to batch those updates, so that multiple state mutations get combined into a single render call?

Synthetic events allow for batches updates

React 17 and lower provides a mechanism to batch those updates. You really don’t have to think about it much while coding, but knowing how React might optimize your code is nevertheless a great learning.

Here’s what happening.

  • multiple state mutations are called within a callback
  • if the callback is attached to a synthetic event, React will batch those mutations
  • when mutations are batched, only a single render-call is made
  • otherwise, each mutation leads to a new rendering

To allow for batching, your state mutations have to be called inside a synthetic React-event. As a quick reminder, synthetic React events are basically just a wrapper around native HTML-events to provide the same functionality across all browsers. Every synthetic event handler can be provided directly to a component as prop.

To see it all in action, here’s an example code that would allow React to combine all state mutations into a single render, instead of one render per mutation.

function Component() {
  const [id, setId] = useState(0);
  const [date, setDate] = useState(new Date());
  const [random, setRandom] = useState(Math.random());

  // A lame callback, but it's just for
  // the demo. Note that clicking on the
  // button will only lead to a single
  // re-render!
  //
  // This is b/c the callback is provided to
  // 'onClick', a synthetic event.
  const onButtonClick = () => {
    setDate(new Date());
    setId(prevId => prevId + 1);
  }
  
  console.log("Render:", id, date);

  return (
    <div>
      <button 
        onClick={onButtonClick}>
        Update state
      </button>
    </div>
  );
}
//
// Almost the same as the previous
// example, but this time w/o
// a batched upate call.
//

function Component() {
  const [id, setId] = useState(0);
  const [date, setDate] = useState(new Date());
  const [random, setRandom] = useState(Math.random());

  // No batching, as the state
  // mutations happend inside
  // a setTimeout - even though
  // the timeout is inside a 
  // synthetic event.
  const onButtonClick = () => {
    setTimeout(() => {
      setDate(new Date());
      setId(prevId => prevId + 1);
    }, 0);
  }
  
  console.log("Render:", id, date);

  return (
    <div>
      <button 
        onClick={onButtonClick}>
        Update state
      </button>
    </div>
  );
}

Batched updates for React 18 and later

With the next version of React (at least while I’m writing this), the batching-mechanism will be much more improved. Instead of only being possible in synthetic events, other types of callbacks will be usable as well.

  • timeouts
  • promises
  • native event handlers

This change will most likely lead to a better performance of your app, as less rendering will be done automatically.

Avoiding batched updates in React 18 and later

Starting with React 18, you will have the option to explicitly trigger a new render with each state mutation, therefore avoiding a batched update. This will most likely only be used in edge cases, but nonetheless it’s interesting to know.

//  
// This example is almost
// 1:1 from Dan Abramov's
// example.
//
// Use 'flushSync' from the
// 'react-dom'-package.
import { flushSync } from 'react-dom'; 

function handleClick() {
  flushSync(() => {
    setId(prevId => prevId + 1);
  });
  // React has updated the DOM by now
  flushSync(() => {
    setDate(new Date());
  });
  // React has updated the DOM by now
}

Suggestions

Related

Addendum

Languages