'React hooks: Why do several useState setters in an async function cause several rerenders?

This following onClick callback function will cause 1 re-render:

const handleClickSync = () => {
  // Order of setters doesn't matter - React lumps all state changes together
  // The result is one single re-rendering
  setValue("two");
  setIsCondition(true);
  setNumber(2);
};

React lumps all three state changes together and causes 1 rerender.

The following onClick callback function, however, will cause 3 re-renderings:

const handleClickAsync = () => {
  setTimeout(() => {
    // Inside of an async function (here: setTimeout) the order of setter functions matters.
    setValue("two");
    setIsCondition(true);
    setNumber(2);
  });
};

It's one re-render for every useState setter. Furthermore the order of the setters influences the values in each of these renderings.

Question: Why does the fact that I make the function async (here via setTimeout) cause the state changes to happen one after the other and thereby causing 3 re-renders. Why does React lump these state changes together if the function is synchronous to only cause one rerender?

You can play around with this CodeSandBox to experience the behavior.



Solution 1:[1]

If code execution starts inside of react (eg, an onClick listener or a useEffect), then react can be sure that after you've done all your state-setting, execution will return to react and it can continue from there. So for these cases, it can let code execution continue, wait for the return, and then synchronously do a single render.

But if code execution starts randomly (eg, in a setTimeout, or by resolving a promise), then code isn't going to return to react when you're done. So from react's perspective, it was quietly sleeping and then you call setState, forcing react to be like "ahhh! they're setting state! I'd better render". There are async ways that react could wait to see if you're doing anything more (eg, a timeout 0 or a microtask), but there isn't a synchronous way for react to know when you're done.

In the current version of react, you can tell react to batch multiple changes by using unstable_batchedUpdates:

import { unstable_batchedUpdates } from "react-dom";

const handleClickAsync = () => {
  setTimeout(() => {
    unstable_batchedUpdates(() => {
      setValue("two");
      setIsCondition(true);
      setNumber(2);    
    });
  });
};

Once react 18 arrives, this won't be necessary, since the changes they've made to rendering for concurrent mode will get rid of the need for this.

Solution 2:[2]

Right now react only batches sync setStates inside event handlers. But in react 18 it will be available in setTimeout, useEffects etc Here is excellent explanation from Dan https://github.com/reactwg/react-18/discussions/21

Solution 3:[3]

UPDATE: REACT 18 FEATURES AUTOMATIC BATCHING

In React, Batching helps to reduce the number of re-renders that happen when state changes, when you call setState(). Previously, React batched state updates in event handlers, for example :

const handleClick = () => {
setCounter();
setActive();
setValue();
}

//re-rendered once at the end.

However, state updates that happened outside of event handlers were not batched. For example, if you had a promise or were making a network call, the state updates would not be batched. Like this:

fetch('/network').then( () => {
setCounter(); //re-rendered 1 times
setActive();  //re-rendered 2 times
setValue();   //re-rendered 3 times
});

//Total 3 re-renders

As you can tell, this is not performant. React 18 introduces automatic batching which allows all state updates – even within promises, setTimeouts, and event callbacks – to be batched. This significantly reduces the work that React has to do in the background. React will wait for a micro-task to finish before re-rendering.

Automatic batching is available out of the box in React, but if you want to opt-out you can use flushSync.

More Reading from FREECODECAMP RESOURCES: https://www.freecodecamp.org/news/react-18-new-features/

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 zhulien
Solution 2 zhulien
Solution 3 Imran Rafiq Rather