Skip to main content

Dependency Array in useEffect: Control When Effects Run (Part 1)

The dependency array is the second argument to useEffect that controls when your side effects run. Pass nothing (runs every render), an empty array (runs once on mount), or list dependencies (runs when they change). Mastering this pattern is essential for writing efficient, bug-free React components.

Key Takeaways

  • No array (no second arg): Effect runs after every render (rarely wanted; causes performance issues)
  • Empty array []: Effect runs once, immediately after initial mount (perfect for data fetching, setup)
  • Dependencies [userId, count]: Effect runs on mount and whenever any listed dependency changes (syncs to state/props)
  • Forgetting dependencies or including wrong ones is the #1 source of useEffect bugs

The Three Modes of useEffect with Dependency Arrays

The dependency array is a JavaScript array passed as the second argument to useEffect. It controls whether React re-runs your effect after a render.

useEffect(() => {
// Effect code
}, [dependencies]); // This is the dependency array

Three rules govern its behavior:

  1. No array → Effect runs after every render
  2. Empty array [] → Effect runs once, on mount only
  3. Array with values [x, y] → Effect runs on mount, then whenever x or y changes

Scenario 1: No Dependency Array (Runs Every Render)

Omit the second argument entirely:

import React, { useState, useEffect } from 'react';

function EffectOnEveryRender() {
const [count, setCount] = useState(0);

useEffect(() => {
console.log(`Effect ran! Count is now ${count}`);
// No second argument = runs after every render
});

return (
<div>
<p>Open console to see effect logs</p>
<button onClick={() => setCount(count + 1)}>
Increment ({count})
</button>
</div>
);
}

export default EffectOnEveryRender;

What happens:

  • Initial render → effect logs "Effect ran! Count is now 0"
  • Click button → re-render → effect logs "Effect ran! Count is now 1"
  • Click again → re-render → effect logs "Effect ran! Count is now 2"
  • And so on...

Performance problem: If your effect makes an API call, you're fetching data on every single render—potentially 10× per second. Almost never what you want.

When to use: Debugging/logging only. Never in production.

Scenario 2: Empty Dependency Array (Runs Once on Mount)

Pass an empty array [] as the second argument:

import React, { useState, useEffect } from 'react';

function FetchOnMount() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
console.log('Fetching data on mount...');

// Simulate fetching (in real apps, use fetch or axios)
setTimeout(() => {
setData({ message: 'Data loaded!' });
setLoading(false);
}, 2000);
}, []); // Empty array = run once

return (
<div>
{loading ? <p>Loading...</p> : <p>{data.message}</p>}
</div>
);
}

export default FetchOnMount;

What happens:

  • Component mounts → effect runs once → logs "Fetching data on mount..." → sets data after 2 seconds
  • Component re-renders → effect does NOT run again
  • Effect runs only once in the component's entire lifetime

When to use:

  • Fetching initial data
  • Setting up timers or intervals
  • Registering global event listeners
  • Initializing a connection (WebSocket, etc.)

Scenario 3: Dependencies (Runs on Mount + When Dependencies Change)

List specific state or props in the dependency array:

import React, { useState, useEffect } from 'react';

function UserDataFetcher({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
console.log(`Fetching user ${userId}...`);
setLoading(true);

// Fetch user data from API
fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
.then(response => response.json())
.then(data => {
setUser(data);
setLoading(false);
});
}, [userId]); // Effect depends on userId

if (loading) return <div>Loading...</div>;

return (
<div>
<h2>{user?.name}</h2>
<p>Email: {user?.email}</p>
</div>
);
}

// Parent component that changes userId
export default function UserSwitcher() {
const [userId, setUserId] = useState(1);

return (
<div>
<button onClick={() => setUserId(1)}>User 1</button>
<button onClick={() => setUserId(2)}>User 2</button>
<button onClick={() => setUserId(3)}>User 3</button>
<UserDataFetcher userId={userId} />
</div>
);
}

Execution timeline:

  • Component mounts with userId={1} → effect runs → fetches User 1
  • Click "User 2" → userId prop changes to 2 → effect runs → fetches User 2
  • Click "User 3" → userId prop changes to 3 → effect runs → fetches User 3

Rule: React compares old and new dependency values using strict equality (===). If any dependency changed, the effect re-runs.

Multiple Dependencies

You can depend on multiple values:

useEffect(() => {
// This effect runs when title OR theme changes
document.title = `${title} | ${theme}`;
}, [title, theme]); // Two dependencies

If either title or theme changes, the effect re-runs.

Practical Example: Search with Debounce

import React, { useState, useEffect } from 'react';

function SearchUsers({ query }) {
const [results, setResults] = useState([]);

useEffect(() => {
if (query.length < 2) {
setResults([]);
return; // Don't search for very short queries
}

console.log(`Searching for "${query}"...`);

const timer = setTimeout(() => {
fetch(`https://api.example.com/search?q=${query}`)
.then(r => r.json())
.then(data => setResults(data));
}, 500); // Wait 500ms after user stops typing

// Cleanup: cancel the search if query changes again
return () => clearTimeout(timer);
}, [query]); // Re-run when query changes

return (
<ul>
{results.map(r => <li key={r.id}>{r.name}</li>)}
</ul>
);
}

Frequently Asked Questions

What if I list a dependency but forget to include it?

React may use stale values, leading to bugs. For example, if your effect uses count but doesn't list it in the dependency array, the effect will always see the initial count value (0) even after you increment it. ESLint catches this with the exhaustive-deps rule.

Can I use objects or functions as dependencies?

Technically yes, but be careful. Objects and functions are compared by reference, not value. Every render creates a new reference, so your effect might run more than expected:

// PROBLEM: This creates a new object every render
useEffect(() => {
console.log(user);
}, [{ name: 'John' }]); // New object each render = effect runs every time

Solution: Memoize the object/function with useMemo or useCallback, or rely on simple dependencies like strings, numbers, and booleans.

Can I use useEffect without a dependency array?

Yes, but it runs after every render (Scenario 1 above). Rarely what you want. Always include a dependency array unless you specifically need the effect on every render (which is uncommon).

How do I clean up an effect (remove timers, listeners)?

Return a cleanup function from useEffect:

useEffect(() => {
const timer = setInterval(() => console.log('tick'), 1000);

// Cleanup: runs when component unmounts or before the effect re-runs
return () => clearInterval(timer);
}, []);

What's the difference between useEffect([]) and useEffect(() => {}, [])?

None. Both mean the same thing: the effect depends on an empty array and runs once on mount.

Further Reading