5

There are two useState hooks used in a component. If we allow for the event loop to "reflow" before modifying the state, each hook causes a separate re-render.

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

export default function App() {
  const [foo, setFoo] = useState("initial_foo");
  const [bar, setBar] = useState("initial_bar");

  const onAction = useCallback(async () => {
      await sleep(0); // this is the culprit
      setFoo("updated_foo");
      setBar("updated_bar");
  }, []);

  console.log(`rendering foo:${foo} bar:${bar}`);

  return (
    <>
      <button onClick={onAction}>run test</button>
      <div>{"foo: " + foo}</div>
      <div>{"bar: " + bar}</div>
    </>
  );
}

console output with await sleep(0):

rendering foo:initial_foo bar:initial_bar
rendering foo:updated_foo bar:initial_bar
rendering foo:updated_foo bar:updated_bar

If we remove await sleep(0);, everything works as expected: setFoo and setBar cause a single re-render.

console output without await sleep(0):

rendering foo:initial_foo bar:initial_bar
rendering foo:updated_foo bar:updated_bar

The demo (in the demo console output is duplicated, because it has React strict mode on)

  • Why is it happening?
  • Is it a bug or a feature?
  • How can we prevent intermediate re-renders of this kind?

Update:

Issue has been created in the React project repo

Boann
  • 48,794
  • 16
  • 117
  • 146
Daniel
  • 1,431
  • 2
  • 16
  • 36
  • I would guess that when the promise is resolved one of the internal references of the functions in the promise is overwritten so the function inside the useCallback() is no longer identical to its original value, and the trigger that is causing the re-render is comparing the current callback with the original callback and fires the re-render because they are different... but that is just a guess – Nathanael Aug 31 '20 at 22:29
  • The `useCallback` has an empty dependencies array, so that shouldn't be the case right? – CollinD Aug 31 '20 at 22:52
  • 3
    See [this answer to _Does React batch state update functions when using hooks?_](https://stackoverflow.com/a/55523100) – Lionel Rowe Sep 01 '20 at 12:23

1 Answers1

0

In promises (and timers for that matter) setState is synchronous. So, you have two rerenders (first for setFoo and then for setBar) instead of combined (batched) one.

Here's an example:

If you click on setState in Promise button you'll get rerender logged after each state, but if you click on setState in an eventHandler you'll get two setState logs first and then there's only one rerender. This behavior should change in React 17 where sate updates will only be asynchronous.

function App() {
  const [foo, setFoo] = React.useState(); 
  const [bar, setBar] = React.useState();
  function handlerPromise() {
    Promise.resolve().then(() => {
      console.log("setFoo");
      setFoo({});
      console.log("setBar");
      setBar({});
    }) 
  }
      function handler() {
      console.log("setFoo");
      setFoo({});
      console.log("setBar");
      setBar({});

  }
  console.log("rerender");
  return (
  <div>
  <button onClick={handlerPromise}>setState in Promise</button>
  <button onClick={handler}>setState in an eventHandler</button>
  </div>
  )
}



ReactDOM.render(<App/>, document.getElementById("root"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.development.js"></script>

<div id="root"></div>
marzelin
  • 10,790
  • 2
  • 30
  • 49