chief dot101
chief dot101

Reputation: 111

Confusing React useEffect hook behaviour

I'm new to React and just practicing useEffect hooks after going through a hooks course.

I have a button to toggle state change of arrowState.

export default function Test() {
  const [arrowState, setArrowState] = useState(true);
  const [arrowChange, setArrowChange] = useState(false);

  
  const toggleButton = () => {
    return setArrowState((arrowState) => {
      return !arrowState;
    });
  };

  return (
    <>
      <div>
        <button
          type="button"
          onClick={toggleButton}
        >
          Toggle Arrow
        </button>
      </div>
      {arrowChange && <Notific />}
    </>
  );
}

Then I have a useEffect hook which changes state of arrowChange. Also have a timeout to change state of arrowChange to false - thereby auto-disappearing the button after 2 secs.

useEffect(() => {
    console.log("first line of useEffect.arrowChange is ", arrowChange);
    setArrowChange(true);
    console.log(
      "immediately after setArrowChange to true. arrowChange is ",
      arrowChange
    );

    setTimeout(() => {
      console.log("first line of setTimeOut. arrowChange is ", arrowChange);
      arrowChange && setArrowChange(false);
    }, 1000);

    return () => {
      console.log("cleaning up arrowchange. arrowChange is ", arrowChange);
      setArrowChange(false);
    };
  }, [arrowState]);

Based on it, I'm displaying another button with a message.

function Notific() {
    return (
      <button
        type="button"
        onClick={() => setArrowChange(false)}
      >
        Arrow state has been changed
      </button>
    );
  }

As you can see, I'm logging some messages to see the effect. I'm surprised to see that even after setting arrowChange to true at the beginning of useEffect hook, it's still showing false at alternate events.

Check these console logs:

cleaning up arrowchange. arrowChange is  false
Test.js:57 first line of useEffect.arrowChange is  true
Test.js:59 immediately after setArrowChange to true. arrowChange is  true
Test.js:65 first line of setTimeOut. arrowChange is  true

Test.js:70 cleaning up arrowchange. arrowChange is  true
Test.js:57 first line of useEffect.arrowChange is  false
Test.js:59 immediately after setArrowChange to true. arrowChange is  false
Test.js:65 first line of setTimeOut. arrowChange is  false

Test.js:70 cleaning up arrowchange. arrowChange is  false
Test.js:57 first line of useEffect.arrowChange is  true
Test.js:59 immediately after setArrowChange to true. arrowChange is  true
Test.js:65 first line of setTimeOut. arrowChange is  true

Test.js:70 cleaning up arrowchange. arrowChange is  true
Test.js:57 first line of useEffect.arrowChange is  false
Test.js:59 immediately after setArrowChange to true. arrowChange is  false
Test.js:65 first line of setTimeOut. arrowChange is  false

Test.js:70 cleaning up arrowchange. arrowChange is  false
Test.js:57 first line of useEffect.arrowChange is  true
Test.js:59 immediately after setArrowChange to true. arrowChange is  true
Test.js:65 first line of setTimeOut. arrowChange is  true

Upvotes: 3

Views: 206

Answers (1)

Yousaf
Yousaf

Reputation: 29282

To understand the output of the code, you need to understand the fundamental concepts of how react updates the state and the concept of a closure.

State updates are asynchronous

Every call to state setter function such as setArrowChange is scheduled - state is not updated immediately.

As a result, logging the value of arrowChange immediately after calling setArrowChange will log the old value of arrowChange.

State is constant within a particular render of a component

State is constant; this means that irrespective of how many times you call the state setter function, component can't see the updated state until it re-renders.

Callback function of setTimeout has a closure over the state

When you create a function is javascript, it forms a closure over its surrounding scope.

In your case, the callback function of setTimeout will log the value of arrowChange was in-effect when the callback function was created.

If at the time of setTimeout call, arrowChange is false, and before the timer expires, you update the state to true, the callback function will still log false because of the closure.

Cleanup function of the useEffect hook has a closure over the state

The cleanup function also has a closure over the arrowChange; it logs the value of arrowChange that was in-effect when the cleanup function was created and returned from the callback function of the useEffect hook.

Now let's take a look at the first group of console.log output.

first group:

cleaning up arrowchange. arrowChange is false 
Test.js:57 first line of useEffect.arrowChange is true 
Test.js:59 immediately after setArrowChange to true. arrowChange is true
Test.js:65 first line of setTimeOut. arrowChange is true

First line of output

cleaning up arrowchange. arrowChange is  false

is from the cleanup function of the useEffect hook that executes:

  • Before running the useEffect again
  • Before the component unmounts

As explained above, it logs the value of the arrowChange which it closed over when the cleanup function of the useEffect was created. Output of the cleanup function suggests that:

  1. Value of the arrowChange was false when the cleanup function was created and returned from the useEffect hook's callback function
  2. The initial value of the arrowChange is false, so this output if from the cleanup function that was most likely created when the useEffect hook executed for the first time, after the initial render of the component

Next three lines

Test.js:57 first line of useEffect.arrowChange is  true
Test.js:59 immediately after setArrowChange to true. arrowChange is  true
Test.js:65 first line of setTimeOut. arrowChange is  true

log the latest value of arrowChange which is most likely because of setArrowChange(true) call in the second line of the useEffect hook that executed after the initial render of the component.

If you observe the output of other groups of console.log outputs, you will notice that the value of arrowChange logged by the cleanup function is opposite to the value logged by the console.log statements inside the callback function of the useEffect hook.

The reason for this is that the cleanup function is from the previous execution of the useEffect hook and it logs the value of arrowChange that was in-effect during the previous execution of the useEffect hook.

console.log statements inside the callback function of the useEffect hook log the latest value of the arrowChange and in the next execution of the useEffect hook, this latest value will become the previous value and the opposite of this latest value will become the latest value.

Upvotes: 4

Related Questions