Home > other >  Missing dependencies in useEffect() really cause stale data?
Missing dependencies in useEffect() really cause stale data?

Time:11-09

I'm asking this question to confirm my understanding of some concept.

The React doc is emphatic about including all dependencies used in the useEffect() callback. As explained in the doc:

Otherwise, your code will reference stale values from previous renders.

I kind of understand where this explanation is coming from. But what concerns me is the "stale value" part. I don't see any possible way stale values can occur due to a missing dependency. My argument is also backed by what's in the doc:

Experienced JavaScript developers might notice that the function passed to useEffect is going to be different on every render. This is intentional. In fact, this is what lets us read the count value from inside the effect without worrying about it getting stale.

As for my understanding, if we miss listing a dependency, the effect will not run after a render caused by that dependency change cuz React doesn't think the effect depends on it. If I take a guess, it may be the situation when the doc refers to as referencing stale data. Indeed, that data is stale in the effect code. However, the effect callback doesn't run in the first place. I won't notice that data being stale until the effect runs. If that matters, I will first figure out why the effect didn't run and resolve the issue. I'd be confounded not by the data being stale, but by the fact that the effect wouldn't run.

What's more, let's suppose the effect runs after a render caused by another dependency change. In this scenario, even if we missed a dependency, we wouldn't read stale data thanks to the aforementioned closure reason. I experimented a bit to confirm this:

export default function App() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(10);

  useEffect(() => {
    console.log(count2);
  }, [count1]);
  
  return (
    <div className="App">
      <div>
        Count1: {count1}
        <button onClick={() => setCount1(count1   1)}>increase</button>
      </div>
      <div>
        Count2: {count2}
        <button onClick={() => setCount2(count2   1)}>increase</button>
      </div>
    </div>
  );
}

We always get the latest count2, as long as the effect runs. So does my understanding holds?

I wanna know why React recommends the inclusion of all dependencies so much. People typically use the dependency array with the intention to bypass some effect running. If they omit a dependency, it's likely what they want. If it's a slip, they will easily notice the effect not running and take action.

CodePudding user response:

Your example code is showing the simplest example, in an extremely simple scenario. By default, useEffect will run on every rerender of your component. Using the dependency array, the internal function only runs when one of those values changes. I have personally run into scenarios where I forgot a dependency, my effect fired, and a value in my function had stale data. This is likely because there were several bits of process going on at once, when the one change triggered the effect, and the other piece of data hadn't caught up yet. Switching to using useReducer for controlling multiple bits of state simultaneously, instead of multiple useState, helped in some of those situations, but ultimately the dependency array kept it in line. Also (and I haven't confirmed this), the framework code for useEffect probably makes heavy use of closures so, again, it's about making sure it's referencing the data points at the right juncture in process.

CodePudding user response:

I've slightly modified your example to show stale values.

Effects are often used for async reasons, so this is not unusual.

Basically it's down to closures, a first render of useEffect will create a closure on count1 & count2, if the effect is not re-run on all dependencies then these closures will stay (stale).

Clicking on count1 then means the useEffect is called again, a new instance of the setInterval is created, with a fresh (none stale) copy of count1 & count2. Because count2 is not in the dependency array, Clicking on count2 will mean the a new setInterval is not created, and stale copies of count1 & count2 are kept in memory.

To be fair, this is probably one area of Hooks that can be tricky to understand. It's easy to think of Hook Components as if there classes with data been part of the Object. But in reality hook Components are only render functions, useState / useEffect etc, are kind of side loaded into the render function pipeline. In comparison a React Class Component has it's data stored with the object instance, so this.xyz is never stale.

const {useState, useEffect} = React;


function App() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(10);

  useEffect(() => {
    const tm = setInterval(() => {
      console.log(count1, count2);
    }, 1000);
    return () => clearInterval(tm);
  }, [count1]);
  
  return (
    <div className="App">
      <div>
        Count1: {count1}
        <button onClick={() => setCount1(count1   1)}>increase</button>
      </div>
      <div>
        Count2: {count2}
        <button onClick={() => setCount2(count2   1)}>increase</button>
      </div>
    </div>
  );
}

ReactDOM.render(<App/>,document.querySelector('#mount'));
<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<div id="mount"></div>

<p>Increase count2, see the console not update until you increase count1,.</p>
<p>Add count2 to the dependancy, and then everything will keep in sync</p>
<iframe name="sif1" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

  • Related