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

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.

Leave a Comment