React: Why the count values are different in useEffect and inc event handler?

Viewed 670

In the below code snippet, the component is rendered thrice when user clicks on the button but the count values remain the same.

import React, { useEffect } from 'react';

const wait = () =>
  new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve();
    }, 2000);
  });

export default function App() {
  const [count, setCount] = React.useState(0);

  useEffect(() => {
    console.log('rerendered');
    console.log(count);
  });

  const inc = async () => {
    setCount(count + 1);
    setCount(count + 5);

    await wait();

    console.log(count);
    setCount(count + 1);
    console.log(count);
    setCount(count + 2);
    console.log(count);
  };

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={inc}>Increment</button>
    </div>
  );
}

I know that first two setCount() are batched and after await they are not batched. If I use setCount(prev => prev +1), I will get the updated values but my question is

Why are the count values different in inc() and useEffect()? When does the count value actually changes coz certainly not on re-render?

1 Answers

Short explanation:

The count value of 0 is closed over the inc function at the moment of execution (i.e. the moment the user pressed the Increment button). This is because the inc function opens a closure with a "frozen" value of 0. Therefore, count will always have the value 0 inside of the event handler function and all of the setCount calls will use a count value of 0 (even if the screen re-renders in-between).

The useEffect hook is called after React rendered. Therefore you'll there log the updated value (after setCount was called) each time.

The setCount calls which happen after 2 seconds will however still use the "old" count value of 0 each time because that's the value which got frozen in the inc closure.


Detailed explanation of your code:

I added some string arguments to your console.log statements to better differentiate between them:

import React, { useEffect } from "react";
import "./styles.css";

const wait = () =>
  new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve();
    }, 2000);
  });

export default function App() {
  const [count, setCount] = React.useState(0);

  useEffect(() => {
    console.log("rerendered - useEffect", count);
  });

  const inc = async () => {
    setCount(count + 1);
    setCount(count + 5);

    await wait();

    console.log("inc 1", count);
    setCount(count + 1);
    console.log("inc 2", count);
    setCount(count + 2);
    console.log("inc 3", count);
  };

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={inc}>Increment</button>
    </div>
  );
}

This gives me the following console output when pressing the Increment button once:

console log output

Explanation:

  1. "rerendered - useEffect 0": useEffect is called on mount of the component with the initial value 0 of the count state.
  2. "rerendered - useEffect 5": Right after pressing Increment the two batched setCount calls cause the value to be 5 and re-render.
  3. Now the inc function waits for 2 seconds.
  4. inc 1 0: After 2 seconds the async function continues its execution and this log is called.
  5. inc 2 0: The setCount call right after inc 1 0 is called before this console.log statement, but executed asynchronously, so it will run after all synchronous tasks (this log), but before the next asynchronous task.
  6. "rerendered - useEffect 1": setCount caused a re-render. 2 is shown in the UI and the useEffect is executed with this new value 2. Why is the value not 7 (i.e. 5 + 2)? - Because the inc function closes over the count value at the moment its called (i.e. when the user pressed the Increment button). Even new re-renders won't alter the count inside of the closure.
  7. inc 4 0: Execution continues and the local "frozen" count value gets printed to the screen.
  8. "rerendered - useEffect 1": Same explanation as point 6.

Play around with the code in this CodeSandBox

Also check out this great article about the useEffect hook which explains its relation with JavaScript closures really well.

Related