5

I am trying to figure out when the re-render occurs when updating state in React with useState hook. In the code below, clicking the button triggers the handleClick function which contains a setTimeout. The callback inside setTimeout is executed after 1 second, which updates the state variable count by calling setCount. A console log then prints a message.

The order I would expect the console logs to show up once the button is clicked are:

  1. 'Count before update', 0
  2. 'Count post update in setTimeout', 0
  3. 'Count in render', 1

However the order I see after running this code is:

  1. 'Count before update', 0
  2. 'Count in render', 1
  3. 'Count post update in setTimeout', 0

How is it that "'Count in render', 1" shows up before "'Count post update in setTimeout', 0"? Doesn't setCount result in the scheduling of a re-render that is not immediate? Shouldn't the console log immediately after the setCount function call always execute before the re-render is triggered?

function AppFunctional() {
  const [count, setCount] = React.useState(0);
  const handleClick = () => {
    console.log('Count before update', count);
    setTimeout(() => {
      setCount(count + 1);
      console.log('Count post update in setTimeout', count);
    }, 1000);
  };
  console.log('Count in render', count);
  return (
    <div className="container">
      <h1>Hello Functional Component!</h1>
      <p>Press button to see the magic :)</p>
      <button onClick={handleClick}>Increment</button>
      {!!count && (
        <div className="message">You pressed button {count} times</div>
      )}
    </div>
  );
}

ReactDOM.render(<AppFunctional />, document.querySelector('.react'));
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div class='react'></div>
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320

1 Answers1

6

Doesn't setCount result in the scheduling of a re-render that is not immediate? Shouldn't the console log immediately after the setCount function call always execute before the re-render is triggered?

Under the hood, React optimizes re-renders by queuing and batching them when it can determine that it's safe to do so. When in a function React understands (such as a functional component, or a hook callback), if you call a state setter in one of those functions, React will know that it's safe to delay the state update until its processing is finished - for example, until all effect / memo / etc callbacks have run, and until all components from the original state have been painted onto the screen.

But when you call a state update outside of a built-in React function, React doesn't know enough about its behavior to know when it'll be able to re-render next if it delays the state update. The setTimeout call is not called from inside the React lifecycle, so batching updates and optimizing them is much more difficult - so, rather than React trying to guess how it could be done safely and asynchronously, it re-renders immediately.

CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • Could you clarify 'immediately' in the context of `setTimeout`? Is it running immediately when the timeout is set, immediately before the callback is run, or immediately after? It seems strange to me that `Count in render 1` is being printed - the render seems to know about the change that happened in the settimeout callback. – Gamma032 Jan 28 '22 at 05:34
  • 2
    Immediately, as in `setCount(count + 1); console.log(...` re-renders before the `console.log` on the next line. `Count in render` logs when there's a render. So, it logs on mount, and also whenever the button is clicked (after a delay), because the button click calls the state setter, and state changes cause rerenders. – CertainPerformance Jan 28 '22 at 05:37
  • "when you call a state update outside of a built-in React function" - how do I know which functions are not "built-in React functions?" I have tested an example similar to the one posted above where I define a separate function `doMyUpdate(myfunc)` **outside** the functional component which contains the console logs and logic to update state. Inside `handleClick` I then pass in the state setter to this function as `doMyUpdate(setCount)`. When I click the button React is able to safely delay the re-render. Is the `setTimeout` call different because the callback is pushed to the callback queue? – user18054452 Jan 29 '22 at 05:12
  • I should've said - if you call an *asynchronous* function outside of React. React can optimize with synchronous functions because it knows they'll terminate and yield control back to React's internal code in the end, while a component is being rendered. – CertainPerformance Jan 29 '22 at 05:14
  • Ah, thanks for clarifying - I modified `doMyUpdate(myfunc)` to an async function and noticed that the re-render is triggered immediately only when I add in a `await Promise.resolve()`. Interestingly, the re-render delay behavior is also different when the `await Promise.resolve()` is placed immediately following `setCount` compared after the `console.log` (which is also after `setCount`)... – user18054452 Jan 29 '22 at 06:26