Under the Hood of React useEffect Dependencies
A deep dive into the dependencies array in React.useEffect().
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 dependencies. 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 legitimate assumption if you read 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 forget 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 works on the development of React at Facebook.
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 declared 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, after the initial render. - It must — or as we’ll see later, should — contain the list of values used in the effect. The effect runs after any of these values changes (and after the 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 a dependency 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 certain 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, as I already explained here.
💡Note: As an aside, this is a scenario where an open-source toolchain like Bit can come in handy. Bit is a tool that allows you to break up a component into small, reusable pieces called “components.” With Bit, you can make granular changes to a component without affecting other parts of your application. So, if you need an effect to run only when certain dependencies change, you can isolate that piece of code into a separate Bit component and use it where needed. This way, you can avoid having to include all the dependencies used in the effect and prevent the effect from running each time any of them changes. Lean more 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 want 🔴.
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 no dependencies. Case 2, it does.
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!).

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.
Caveat
There is something to keep in mind though. Since the effect captures the values at the time the component is rendered, any asynchronous code that executes in the effect will also see these values rather than the ‘latest’ values.
Consider this example:
useEffect(() => {
asyncOp(props.value)
.then((result) => setData(result + props.other));
}, [props.value]);
What’s going on here? props.other is used when the promise resolves. But since the effect captures the value of props.value and props.other at the time of render, the value of props.other might not be the ‘latest’. If props.other had changed during the time it took for the promise to resolve, the code would see the ‘old’ value.
Again, this is normal if you’ve understood how useEffect
works, but chances are that this was not what you were after. In that case, you should consider using a reference to store the value of props.other, so your code will always see the latest value.
Alternatively you may also add props.other back to the dependency array and return a cleanup function:
useEffect(() => {
let cancelled = false;
asyncOp(props.value)
.then((result) => {
if (!cancelled) setData(result + props.other);
});
return () => cancelled = true;
}, [props.value, props.other]);
This code ensures that a “cancelled” flag is raised before running the effect again (when props.value or props.other have changed compared with the previous render). So essentially, we’re ignoring the result of the promise when the value of props.other may not be the latest. This is not ideal since asyncOp
had to “run for nothing”, but it works.
Once again, a better solution would be to refactor the code:
const [result, setResult] = useState();useEffect(() => {
asyncOp(props.value).then((result) => setResult(result));
}, [props.value]);const data = result + props.other;
In this solution, the effect is solely used to retrieve the result of asyncOp
when props.value changes, and data is computed at the time of render using props.other, which is always up to date. Much better and much simpler!
Final thoughts
I was really torn about writing this article. This felt like telling you to do things that you were not supposed to do. But I think the React team didn’t get their message across very well 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 “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 that you 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 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 the 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 explicit about your dependencies while giving you more granularity than useEffect
. Just beware that these libraries are not technically necessary.
May 2022 update: the React team just published a RFC proposal for useEvent, which promises to address these issues without having to violate the ESLint rule for exhaustive dependencies. Have a look!
September 2022 update: if you can’t wait for useEvent to be released, you can check its polyfill now:
Build Apps with reusable components, just like Lego

Bit’s open-source tool help 250,000+ devs to build apps with components.
Turn any UI, feature, or page into a reusable component — and share it across your applications. It’s easier to collaborate and build faster.
Split apps into components to make app development easier, and enjoy the best experience for the workflows you want: