Understanding React useEffect Dependency Array: The Complete Guide
If you have been working with React for any amount of time, you have probably encountered the infamous useEffect hook. And if you have used useEffect, you have definitely struggled with the dependency array. Why does my effect run infinitely? Why does not it run when I expect? Why does ESLint keep warning me?
Let us demystify this once and for all.
What is the Dependency Array?
The dependency array is the second argument you pass to useEffect. It is an array of values that your effect depends on. When any of these values change, React will re-run your effect.
useEffect(() => {
// Your effect logic here
}, [dependency1, dependency2]); // This is the dependency array
The Three Rules of Dependency Array
1. Empty Array = Run Once
When you pass an empty array, your effect runs only once after the initial render (similar to componentDidMount):
useEffect(() => {
console.log("Component mounted!");
// This runs only once
}, []);
2. No Array = Run Every Render
When you omit the array entirely, your effect runs after every render:
useEffect(() => {
console.log("Runs after every render!");
// This runs after EVERY render - danger!
});
3. With Dependencies = Run When They Change
Your effect runs when any dependency value changes:
const [count, setCount] = useState(0);
useEffect(() => {
console.log("Count changed:", count);
}, [count]); // Runs when count changes
Common Mistakes and How to Fix Them
Mistake #1: The Infinite Loop
This happens when your effect updates a state that is also in the dependency array:
// WRONG - causes infinite loop
const [data, setData] = useState(null);
useEffect(() => {
fetchData().then(result => setData(result));
}, [data]); // Updates data, which triggers effect again!
Fix: Remove the state variable from dependencies if you are setting it in the effect:
// CORRECT
useEffect(() => {
fetchData().then(result => setData(result));
}, []); // Empty array - run once on mount
Mistake #2: Forgetting Dependencies
ESLint will warn you about this. Always include all values that your effect uses:
// WRONG - ESLint will complain
useEffect(() => {
console.log("User:", user.name);
}, []); // Missing user dependency!
// CORRECT
useEffect(() => {
console.log("User:", user.name);
}, [user]);
Mistake #3: Object/Array Dependencies
Objects and arrays in dependencies can cause unnecessary re-renders because they are compared by reference:
// PROBLEMATIC - new object every render
useEffect(() => {
doSomething(config);
}, [config]); // config is { theme: "dark", fontSize: 16 }
const config = { theme: "dark", fontSize: 16 }; // Created new each render!
Fix: Use primitive values or use useMemo:
// BETTER
useEffect(() => {
doSomething(config.theme, config.fontSize);
}, [config.theme, config.fontSize]);
The Cleanup Function
Return a function from your effect to clean up (similar to componentWillUnmount):
useEffect(() => {
const subscription = api.subscribe(data => {
setData(data);
});
// Cleanup function
return () => {
subscription.unsubscribe();
};
}, []);
Quick Reference Table
| Dependency Array | When It Runs |
|---|---|
| No second argument | After every render |
| Empty array [] | Only on mount (once) |
| [value1, value2] | When value1 or value2 changes |
Pro Tips
- Use ESLint exhaustive-deps rule - It will catch missing dependencies
- Prefer primitive values in dependencies when possible
- Use useCallback to stabilize function references
- Use useMemo to stabilize object/array references
- Move logic inside effect if it only affects the component
Conclusion
The dependency array is one of the most powerful yet confusing parts of useEffect. Remember: it is React way of knowing when to re-run your side effects. Treat it with respect, and your components will behave predictably.
Master this, and you will eliminate countless hours of debugging mysterious re-renders and infinite loops.