React useEffect Dependencies
Table of Contents + −
In the last lesson, React useEffect on Mount, you saw that an empty array runs the effect once. This lesson is about the dependency array, the part that re-runs an effect at exactly the right moments instead.
🤔 Why the dependency array exists
Here’s the pain. An effect often uses some value from your component, and you want the effect to stay in sync with that value.
- Say your effect updates the document title using
count. - When
countchanges, the title is now out of date. The effect needs to run again. - But you don’t want it running on every render, because most renders have nothing to do with
count.
So you need a way to tell React: “only re-run this effect when these specific values change.” That’s exactly what the dependency array is for.
🧩 What the dependency array does
The second argument to useEffect is an array of values React watches between renders.
- After every render, React looks at the values in the array.
- If any of them changed since the last render, it runs your effect again.
- If none changed, it skips the effect for that render.
This effect re-runs only when count changes, because count is the one value listed.
import { useEffect } from "react";
useEffect(() => { document.title = `You clicked ${count} times`;}, [count]);So the array is like a small watch-list. React keeps an eye on count, and the title updates the moment it changes.
🔍 How React compares the values
So how does React decide if a value “changed”? It compares each item to its value from the last render.
- React uses
Object.isto compare each dependency, which is basically a strict equality check. - For simple values like numbers, strings, and booleans, this just compares the actual value. So
5equals5. - The comparison is shallow, meaning React only checks the top-level value, never the contents inside an object or array.
This is the key idea. For numbers and strings it behaves exactly how you’d expect.
// last render: count was 0// this render: count is 1// Object.is(0, 1) is false -> the effect runs againSo this is simple and predictable for plain values.
- When
countgoes from0to1, React sees they’re different and runs the effect. - When
countstays0across two renders, it sees they’re the same and skips it.
📝 List every value the effect uses
Here’s the main rule, and it’s a simple one. Every value from inside your component that the effect reads must go in the array.
- That means any state, any prop, or any other variable from the component that the effect touches.
- If the effect uses it, list it. No exceptions.
- This is what keeps the effect in sync. When a listed value changes, the effect re-runs with the fresh value.
This effect reads two values, count and userName, so both belong in the array.
useEffect(() => { document.title = `${userName} clicked ${count} times`;}, [count, userName]);So the array isn’t a guessing game. You look at what the effect uses and you list those things. The exhaustive word just means “list them all, leave nothing out”.
❌ The missing dependency bug
Now the most common mistake. You leave a value out of the array, and the effect uses an old, frozen copy of it.
- The effect reads
count, but the array is empty[]. - An empty array means “run once and never again”, so the effect never re-runs when
countchanges. - The effect keeps using the
countfrom the very first render, which is0. That’s a stale value.
Look at the broken version next to the fixed one. The only difference is what’s in the array.
// ❌ Wrong - effect uses count but the array is emptyuseEffect(() => { document.title = `You clicked ${count} times`;}, []); // title is stuck on "You clicked 0 times" forever
// ✅ Right - count is listed, so the title updates every timeuseEffect(() => { document.title = `You clicked ${count} times`;}, [count]);See the difference in behavior.
- With the empty array the title freezes on “You clicked 0 times” even as you keep clicking.
- With
[count]in there, React re-runs the effect on each change and the title stays correct.
A missing dependency hides bugs
An empty array looks harmless, but if the effect uses a value that changes, the effect keeps the old copy. The screen and the effect quietly drift apart, and nothing throws an error. These bugs are hard to spot, so always list what you use.
⚠️ The unstable dependency bug
There’s an opposite mistake too. You can put something in the array that looks the same to you but is brand new to React on every render.
- Objects, arrays, and functions created inside the component are made fresh on each render.
- Even if the contents look identical, it’s a new value in memory, so
Object.issays it changed. - React then re-runs the effect on every single render. That’s an unstable dependency.
In this example the options object is rebuilt each render, so React thinks the dependency changed every time.
function Search() { // a new object is created on every render const options = { sort: "asc" };
useEffect(() => { runSearch(options); }, [options]); // runs EVERY render, because options is new each time
return <input />;}So even though { sort: "asc" } looks the same to your eyes, React sees a different object each render.
- The effect fires non-stop, on every render.
- The usual fix is to depend on the simple value inside, like
[options.sort], instead of the whole object. - We’ll cover stabilizing objects and functions properly in later lessons.
🛟 Let the linter help you
You don’t have to track all of this in your head. There’s a tool that checks the array for you.
- The
eslint-plugin-react-hookspackage ships a rule called exhaustive-deps. - It reads your effect, sees which values it uses, and warns you if any are missing from the array.
- Most React setups, like Create React App and Vite templates, turn this rule on by default.
When you forget a dependency, you get a warning right in your editor, something like this.
useEffect(() => { document.title = `You clicked ${count} times`;}, []);// React Hook useEffect has a missing dependency: 'count'.// Either include it or remove the dependency array.So treat that warning as a real hint, not noise.
- Almost every time, the fix is to add the value it names into the array.
- The linter has saved a lot of people from stale-value bugs.
Trust the warning
When exhaustive-deps tells you a dependency is missing, the right move is usually to add it. Don’t silence the warning to make it go away, because that’s exactly how the stale-value bug sneaks back in.
🧩 What You’ve Learned
- ✅ The dependency array is the second argument to
useEffect, and it controls when the effect re-runs - ✅ React re-runs the effect whenever any listed value changes between renders
- ✅ It compares each dependency with
Object.is, and the check is shallow - ✅ Every state, prop, or variable the effect uses must go in the array
- ✅ Leaving a value out gives you a stale value, so the effect uses an old frozen copy
- ✅ Putting a fresh object, array, or function in the array makes the effect run on every render
- ✅ The eslint exhaustive-deps rule warns you when a dependency is missing, so trust it
Check Your Knowledge
Test what you learned. Pick an answer for each question, then click Check.
- 1
When does an effect with [count] re-run?
Why: Listing a value in the array tells React to re-run the effect whenever that value changes. So [count] means the effect runs again each time count changes.
- 2
An effect uses count but its array is empty []. What goes wrong?
Why: With an empty array the effect runs once and never again. It keeps the count from the first render, which is a stale value, so it never reflects later changes.
- 3
Why does putting a fresh object { sort: "asc" } in the array make the effect run every render?
Why: The object is rebuilt on every render, so it is a new value in memory. Object.is reports it changed each time, and the effect re-runs every render.
- 4
What does the exhaustive-deps lint rule do?
Why: The exhaustive-deps rule checks which values your effect uses and warns when any are missing from the array, helping you avoid stale-value bugs.
🚀 What’s Next?
Now you can control exactly when an effect runs. But some effects start something that needs stopping later, like a timer or a subscription, and that’s where the cleanup function comes in.