React Asynchronous Nature, Automatic Batching and Ordering of setState

It is imperative to understand certain concepts around setting State in React that will only help with our ability to write components and their logic better. We will cover three topics mainly:

  • Asynchronous Nature of setting/updating state
  • Automatic Batching of multiple setState
  • Ordering of multiple setState

The code samples I’ll use in this article will use the Hooks API in Functional components but the overall ideas apply similarly to Class components as well.

Why is setState Asynchronous?

Let us first understand what do we mean by setting state being asynchronous with an example:

import { useState } from 'react';

function Component(props) {
  const [count, setCount] = useState(0);

  const handleClick = (e) => {
    setCount(c => c + 1);
  };

  return (
    <>
      <p>Count: {count}</p>
      <button onClick={handleClick}>setCount</button>
    </>
  );
}

The setting or updating of a state value or variable doesn’t set the state immediately. For instance in our example above, if we tried to access the state value right after the setCount call, that wouldn’t give us the updated value, but the old value.

setCount(c => c + 1);
console.log(count); // On the first click, logs 0, not 1!

This is because React doesn’t execute state updates synchronously, i.e., at the time when the update function (setCount) is called. Instead, it queues the update for it to be applied later. Later, when all the other logic or code has been executed and only rendering is pending which in the example above would mean after handleClick is done executing.

Why is that? To be honest, Dan has done an excellent job of explaining this here which you must read. But I will still summarise the important/relevant parts.

Whenever a state is updated with a value different from the previous one, React re-renders the component. We all know this. Now in a particular code flow, like a click event handler (handleClick above), if we were updating ten different state variables and if all of them executed synchronously, then that’d cause ten different re-renders for the component along with any child components. That’s a lot of unnecessary renders.

Also when both child and parent called a setState, that would cause the child to render twice (since re-rendering of a parent component also triggers its children to re-render). That’s bad for performance!

This is the main reason why React decides to execute all setState calls present in an execution flow or code path asynchronously, i.e., at the end when only the re-rendering is left to be done. This ensures only one re-render instead of multiple and is known as “automatic batching” which we will talk more about in a bit.

What is even more important to understand here is that even if React managed to somehow update the states synchronously and postpone renders to happen in the end only once, it would still lead to a major issue in cases of lifting state up where the props won’t be updated. This is an issue when a child component:

  • Calls a prop function that updates parent state and triggers parent + child re-render, or
  • Calls a prop function which is a set state function only.
function ChildComponent(props) {
  // ...

  // Let's imagine props.setCount() is supposed to update
  // a state variable in the parent component which is passed
  // as props.count

  props.setCount(c => c + 1);

  // Even if React managed to synchronously execute `setCount`
  // `props.count` wouldn't be updated without a re-render
  // of parent component. That's a problem because we could see
  // the old props.count render first in a flash followed by the
  // new props.count
  return <p>Count: {props.count}</p>;
}

This example is a fairly simple one to explain the problem, but the props could be in use for much more complex logic.

Updating props is achieved by re-rendering the parent whenever its state changes. And to avoid significant performance degradation every set state call, be it for a parent or the child itself, doesn’t trigger a re-render. Instead, all of them are batched as we discussed earlier.

This way, re-renders make sure that after any state updates, the state, props and refs are all internally consistent between each other referring to a version of the fully reconciled tree.

What is Automatic Batching?

Although we fairly covered automatic batching, let’s see an example to dive deeper. In a lot of cases, we will update multiple states in the same code flow, for instance in a click event handler.

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

  const handleClick = (e) => {
    setCount(c => c + 1);
    setFlag(f => !f);
  };

  return (
    <>
      <p style={{ fontWeight: flag ? 'bold' : 'normal' }}>Count: {count}</p>
      <button onClick={handleClick}>setCount</button>
    </>
  );
}

The setCount and setFlag calls inside handleClick do not re-render the component twice, but only once. This is React’s automatic batching. As discussed in the previous section, this massively improves performance when you’re updating lots of states by causing a single render only. Batching also ensures that there are no UI render bugs based on partial state updates.

So just to make sure we understand it properly, in a set of multiple state update calls, the points of rendering would look like this:

setA();
// No render
setB();
// No render
setC();
// No render
setD(); // Last set state call
// Yes render!

The batching will happen across all set state calls across component hierarchy:

// Batch the following
setA();
props.setB(); // on parent component
setC();
props.setGrandParentD();
props.someFuncThatSetsAPropOnAGrandParent();
// Re-render here

Automatic batching will happen regardless of whether the states are being updated inside a React event handler (handleClick example above), a Promise callback, a timer callback (setTimeout, setInterval) or native event handlers (domNode.addEventListener('...')). It will work everywhere!

In cases where we may want to opt-out of the batching, we can use the ReactDOM.flushSync() API method:

import { flushSync } from 'react-dom';

function Component() {
  // ...

  const handleClick = (e) => {
    flushSync(() => {
      setA();
    });

    // React has re-rendered the component,
    // flushed changes and updated the DOM

    setB();

    // React has re-rendered the component,
    // flushed changes and updated the DOM
  }

 return ...;
}

Dan has an excellent write up on automatic batching as well.

Is the Order of State Updates Maintained?

We have learnt that set state update calls are asynchronous and batched. It might lead to an interesting question, are the order of multiple set state calls maintained? Especially when we use the callback method to update them:

// Will the 2 callbacks passed below be executed (asynchronously later)
// in the same order as they are called ?

setCount((s) => {
  // Lots of logic
});

setFlag((s) => {
  // Lots of logic
});

The answer is a big YES! The order in which multiple setState are called in a single component or across multiple components (hierarchy) is always respected when updating them. Due to batching in most cases, this question should not really affect us because in the end all the state irrespective of the order in which they update, will lead to a single render not causing any unexpected UI bug (imagine render happening based on some “intermediate state”).

But in cases where this question does come to your mind, because of reasons like mutating and using the same object reference across the callbacks passed to setState, you know the answer now. The order is kept or maintained. Although you shouldn’t be passing the same mutable object reference to avoid unexpected and hard-to-debug bugs.

Leave a Reply

Your email address will not be published.