Understanding Dependencies in useEffect

A deep dive into the dependencies array in React.useEffect(). What causes stale values from previous renders in ReactThere is so much confusion around the dependency array used in useEffect that nobody seems to understand what you can and can’t do with…


This content originally appeared on Bits and Pieces - Medium and was authored by Guillaume Renard

A deep dive into the dependencies array in React.useEffect(). What causes stale values from previous renders in React

There is so much confusion around the dependency array used in useEffect that nobody seems to understand what you can and can’t do with them. Let’s untangle this a bit.

First, I have to mention that this a follow up to my previous story: a more granular useEffect. I wrote this story and granular-hooks on the basis that it was not safe to omit dependencies from the dependencies array. This was a legit assumption reading the official documentation:

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.

Pretty clear, isn’t it? Nobody likes to reference stale values. There is even an ESLint plugin to yell at you whenever you forgot to include dependencies. Omit dependencies, and this will happens.

Or will it?

Understanding useEffect

Before we see when it is safe and when it is not to omit dependencies from the array, we need to first understand how effects work (and more generally how functional components work).

One of the best resources — if not the best — is the Complete Guide to useEffect. It’s written by Dan Abramov, who worked on the development of React at Facebook and created Redux.

The key takeaway regarding our problem is:

Every function inside the component render (including event handlers, effects, timeouts or API calls inside them) captures the props and state of the render call that defined it.

So in this code:

const [data, setData] = useState();
useEffect(() => {
const result = expensiveOp(props.value);
setData(result);
}, [props.value]);

When the effect runs, props.value contains the value at the time the component was rendered. If props.value was 2, then expensiveOp will be called with 2 inside the effect, no matter when it runs. It’s as good as if the function was defined with:

() => {
const result = expensiveOp(2);
setData(result);
}

When you declare a function that references a variable outside of its scope, JavaScript creates a closure, which “captures” the value of that variable inside the function. And since props and state are immutable, they will always yield the same value inside the effect: the value at the time the component was rendered.

Now what about the dependency array passed to useEffect? We know from the documentation that:

  • It’s optional. If you don’t specify it, the effect runs after each render.
  • If it’s empty ([]), the effect runs once, at the initial render.
  • It must — or as we’ll see later, should — contain the list of values used in the effect. The effect runs when any of these values changes (and on initial render).
  • The array of dependencies is not passed as argument to the effect function.

The last point is important. It means that the array plays no direct role in ensuring that the effect runs with fresh values (the effect always runs with the value at the time the component is rendered, as we’ve just seen). The array only controls when the effect runs.

With that in mind, let’s review a few scenarios, starting with the easier ones.

Scenarios

Scenario 1: the effect should run each time the component renders

If you want to run an effect whenever the component renders, just omit the list of dependencies:

useEffect(() => {
const result = expensiveOp(props.value);
setData(result);
});

If the component is rendered with 2 in props.value, then the effect runs with 2. If it’s re-rendered with 3, then it runs again with 3 . Easy. That’s because the effect “captures” the value at the time the component is rendered, whether it’s a prop, a state or any variable defined in the scope of the component.

Scenario 2: the effect should run each time a dependency changes

Again, this one is a no brainer. The dependency array should include all the dependencies used in the effect. And the effect will run whenever any of the dependencies in the array changes. This the behaviour that we are all familiar with:

useEffect(() => {
const result = expensiveOp(props.value);
setData(result);
}, [props.value]);

If the component is rendered with 2 in props.value, then the effect runs with 2. If it’s re-rendered again with 2, then it does not run (because the value hasn’t changed). If it’s re-rendered this time with 3 , it runs with 3. Again, the effect “captures” the value at the time the component is rendered so it runs with the expected value.

And if you were wondering, no, you should not omit dependencies from the array in this case. It’s not “safe” to do so in the sense that the effect will not run again if one of the omitted value changes, so you might miss updates, unless this is what you want which bring us to scenario 3 👇

Scenario 3: the effect should run when some dependencies change (but not others)

This is the confusing one. This case is not actually covered in the Complete Guide to useEffect. If you end up in this situation, your first reflex should be to refactor your code to get out of here, as I already explained here.

But if for some reason you have to do this, you will be confronted with the fact that React wants you to list all the dependencies used in the effect, which is annoying, because if you do so, the effect will run each time any of the dependency changes. For example, if we wanted to run expensiveOp(props.value, props.other) only when props.value changed, we could not use this:

useEffect(() => {
const result = expensiveOp(props.value, props.other);
setData(result);
}, [props.value, props.other]);

Here the dependency array is complete ✅, but the effect runs when either of the dependency changes, which is not what we wanted 🔴.

So what if we just ignored the scary warning and omitted props.other from the dependency array?

useEffect(() => {
const result = expensiveOp(props.value, props.other);
setData(result);
}, [props.value]);

Here, if props.value is 2 and props.other is 3 initially, the effect first runs with expensiveOp(2, 3). If props.value changes to 3 the effect next runs with expensiveOp(3, 3). Nothing new.

Now if props.other changes to 4 , what happens? Nothing. The dependency array does not contain props.other, and props.value hasn’t changed. So the effect does not run. Good.

But now, let’s says that props.value changes to 4. Will the effect run with expensiveOp(4, 3) (3 being the “previous” value of props.other — a stale value) or with expensiveOp(4, 4) (4 being the “current” value of props.other — the fresh value)?

Because of what we’ve seen so far, the answer is expensiveOp(4, 4). The effect “captured” the value of props.value and props.other at the time the component was rendered, even though props.other was not included in the dependency array.

Despite the warning, it is technically impossible for your effect to use a stale value.

So yes, if we want the effect to run only when props.value changes, it is safe to omit props.other from the dependency array.

Scenario 4: the effect should only run once

Here we have two cases. Case1: the effect has dependencies. Case 2, it doesn’t.

Case 1. It’s easy, we can call the effect with an empty array of dependencies, as mentioned in the docs:

useEffect(() => {
const result = expensiveOp();
setData(result);
}, []);

The effect will run when the component is mounted (the initial render), and only then.

Note that in this example, expensiveOp is a method imported from outside of the component. It doesn’t count as a dependency because it is constant. But if it was defined in the component scope, it should be considered as a dependency — see case 2.

Case 2. Not so easy. So we have dependencies, but the code should only run once. Again, this might be a code smell, so you should try to refactor first. Dan’s Complete Guide to useEffect has a few tips for that.

But let’s not judge and assume that this has to be done. Will omitting the dependencies from the list work? Of course it will:

useEffect(() => {
const result = expensiveOp(props.value);
setData(result);
}, []);

As long as you understand that the effect will only run once, and that it will run with the initial value of props.value, you get what you asked for. Of course you’ve violated the rule that says that the array should contain all the values used in the effect, but technically speaking, it works.

Wrapping it up

I’ve compiled the list of dependency arrays that you can safely use for the different scenarios (- means that the dependency array is to be omitted). As you can see at first glance, it’s exactly how you would expected it to be (so much for a confusing warning!).

Rows indicate when the effect should run, columns show the actual dependencies of the effect, and cells specify the array of dependencies to be used when calling useEffect.

Now if you look at the table, I have also added some yellow where the dependency array actually violates the rule. When you fall into these cases, please try first to refactor your code first.

Final thoughts

I was really torn about writing this article. This was a bit like telling you to do things that you were not supposed to be doing in the first place. But I think the React team kind of messed up getting their message across and it was necessary to understand what you can and cannot do with useEffect.

When people switched from classes to functional components, they needed to understand that effects did not automagically run with the “current” values (unlike what you get in classes with componentDidMount and componentDidUpdate). The React team had to emphasize that you needed to include the values used in the effects as dependencies to make sure they ran again with “fresh” values.

To conclude, I would still recommend to avoid omitting dependencies from the array. This is not how React intended it. Being exhaustive with the list of dependencies has other advantages, such as making it explicit what your effect depends on and making sure that you are not skipping updates by mistake.

But If you want (or have) to omit some, then make sure you understand what you are doing, add a comment to your code and go ahead. Else, if you’re not comfortable with the warning, you can still use some third party libraries, such as the one I presented earlier, which will encourage you to be explicitly about your dependencies while giving you more granularity than useEffect. Just beware that these libraries are not technically necessary.

Build component-driven. It’s better, faster, and more scalable.

Forget about monolithic apps, start building component-driven software. Build better software from independent components and compose them into infinite features and apps.

OSS Tools like Bit offer a great developer experience for building component-driven. Start small and scale with many apps, design systems or even Micro Frontends. Give it a try →

An independent product component: watch the auto-generated dependency graph

Understanding Dependencies in useEffect was originally published in Bits and Pieces on Medium, where people are continuing the conversation by highlighting and responding to this story.


This content originally appeared on Bits and Pieces - Medium and was authored by Guillaume Renard


Print Share Comment Cite Upload Translate Updates
APA

Guillaume Renard | Sciencx (2022-02-16T10:51:51+00:00) Understanding Dependencies in useEffect. Retrieved from https://www.scien.cx/2022/02/16/understanding-dependencies-in-useeffect/

MLA
" » Understanding Dependencies in useEffect." Guillaume Renard | Sciencx - Wednesday February 16, 2022, https://www.scien.cx/2022/02/16/understanding-dependencies-in-useeffect/
HARVARD
Guillaume Renard | Sciencx Wednesday February 16, 2022 » Understanding Dependencies in useEffect., viewed ,<https://www.scien.cx/2022/02/16/understanding-dependencies-in-useeffect/>
VANCOUVER
Guillaume Renard | Sciencx - » Understanding Dependencies in useEffect. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2022/02/16/understanding-dependencies-in-useeffect/
CHICAGO
" » Understanding Dependencies in useEffect." Guillaume Renard | Sciencx - Accessed . https://www.scien.cx/2022/02/16/understanding-dependencies-in-useeffect/
IEEE
" » Understanding Dependencies in useEffect." Guillaume Renard | Sciencx [Online]. Available: https://www.scien.cx/2022/02/16/understanding-dependencies-in-useeffect/. [Accessed: ]
rf:citation
» Understanding Dependencies in useEffect | Guillaume Renard | Sciencx | https://www.scien.cx/2022/02/16/understanding-dependencies-in-useeffect/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.