Skip to content
Advertisement

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.

Advertisement

Answer

In react 17, 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.

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);    
    });
  });
};

In version 18 this isn’t necessary, since the changes they’ve made to rendering for concurrent rendering make batching work for all cases.

User contributions licensed under: CC BY-SA
10 People found this is helpful
Advertisement