Why is my value from state hook different than what I'm logging in my useEffect?

Viewed 139

If useEffect runs after the render phase, why is my value in useEffect less than what is being shown in the return?

I have a component that will update value whenever my counter changes on cleanup

  const [value, setValue] = useState(0);
  const [counter, setCounter] = useState(0);

  useEffect(() => {
    console.log(`value = ${value} from effect`);
    return () => {
      setValue(v => v + 1);
      console.log(`value = ${value} from cleanup`);
    };
  }, [counter]);

  return (
    <div>
     <button onClick={() => setCounter(v => v + 1)}>Increment Counter</button>
      <p>value: {value}</p>
    </div>
  );

On first increment, the value in my return will be 1, but my useEffect will log it as 0. Why would those values be different and why would the useEffect not log 1 as well? This component doesn't have a real purpose, just something I'm experimenting with

1 Answers

Let's step through your component and look at what's actually happening. I will mark value and counter with their respective render to show which version is used like value_1 for first rerender.

Mount

value_0 = 0, counter_0 = 0.

Effect is run. log value_0. register cleanup using value_0.

return JSX using value_0.

Event: user clicks increment. counter is set to 1.

1. rerender triggered by setCounter

value_1 = 0, counter_1 = 1.

Compare deps, [0] elements differ from [1]. Cleanup is run. log value_0. set value to 1. Effect is run. log value_1. register cleanup using value_1.

return JSX using value_1.

2. rerender triggered by setValue

value_2 = 1, counter_2 = 1.

Compare deps, [1] elements match [1].

return JSX using value_2.

-

You will notice that value and counter are constants for each render. They are defined as const after all. Whenever cleanup is run, the function that is executed is the one returned by the last render, which used its own version of value.

The effect is never run on the last update where value is finally up to date because the deps haven't changed.

You can avoid this kind of confusion by using the react-hooks/exhaustive-deps lint rule. It will stop you accidentally using out-of-date values in your effects, callbacks or memoised values.

Related