Search code examples
reactjsreact-hooksuse-effect

useEffect dependency list and its best practice to include all used variables


According to the react docs at https://reactjs.org/docs/hooks-reference.html#conditionally-firing-an-effect when using the useEffect dependency array, you are supposed to pass in all the values used inside the effect.

If you use this optimization, make sure the array includes all values from the component scope (such as props and state) that change over time and that are used by the effect. Otherwise, your code will reference stale values from previous renders. Learn more about how to deal with functions and what to do when the array values change too often.

I don't know how the hook works behind the scenes, so I'm going to guess here. Since the variables inside the closure might go stale, that would imply that the function is cached somewhere. But why would you cache the function since its not being called unless the dependencies changed and the function needs to be recreated anyways?

I've made a small test component.

function App() {
  const [a, setA] = useState(0);
  const [b, setB] = useState(0);

  useEffect(() => {
    console.log("b", b);
  }, [a]);

  return (
    <div>
      <div>App {a}</div>
      <button
        onClick={() => {
          setA(a + 1);
        }}
      >
        AddA
      </button>
      <button
        onClick={() => {
          setB(b + 1);
        }}
      >
        AddB
      </button>
    </div>
  );
}

You can try it out here: https://codesandbox.io/s/react-hooks-playground-forked-m5se8 and it works just fine, with no stale values. Can someone please explain what I'm missing?

Edit: After feedback that my question is not entirely clear, adding a more specific question:

When after a page load I click on AddB button and then click on AddA button, value displayed in console is 1. According to the docs, I should get a stale value (0). Why is this not the case?


Solution

  • When after a page load I click on AddB button and then click on AddA button, value displayed in console is 1. According to the docs, I should get a stale value (0). Why is this not the case?

    The reason why you don't see stale value in that case is that when you click AddA a re-render happens. Inside that new render since value of a is different from previous render, the useEffect will be scheduled to run after react updates the UI (however the values it will reference will be from current render - because the function passed to useEffect is re-created each time one of its dependencies change, hence it captures values from that render).

    Due to above reasons, that is why you see fresh value of b.

    But if you had such code

    React.useEffect(() => {
        const timer = window.setInterval(() => {
           setB(b + 1);
        }, 1000);
        return () => {
          window.clearInterval(timer);
        };
      }, []);
    

    b would be a stale closure. Since useEffect didn't re-run, b always has value from the render it was created in, that is the first render and above code wouldn't work as expected, which you can check yourself.


    I will add this comment by Patrick Roberts from above because it explains well IMHO what react docs may mean when they say "Otherwise, your code will reference stale values from previous renders":

    the problem is that you had to click on AddA button in the first place. Between the time you click AddB and AddA, the effect references b from a stale closure