Dependency Array
Overview
By default, the useEffect hook runs after the initial render AND after every single subsequent re-render of the component. If a component re-renders 50 times, the effect runs 50 times. This is almost never what you want (e.g., you don't want to fetch user data from the API 50 times).
To control exactly when the effect runs, you must pass a second argument to useEffect: the Dependency Array.
The dependency array is a list of state variables or props that the effect 'depends' on. React will look at this array before running the effect. If none of the variables in the array have changed since the last render, React skips the effect entirely, saving massive amounts of processing power and network bandwidth.
Syntax
// Mode 1: NO Array (Runs on EVERY render) - Very Rare
useEffect(() => { console.log('Runs constantly!'); });
// Mode 2: EMPTY Array (Runs EXACTLY ONCE on mount) - Very Common
useEffect(() => {
console.log('Component Loaded! Fetching initial data...');
}, []);
// Mode 3: POPULATED Array (Runs on mount AND when dependencies change) - Very Common
useEffect(() => {
console.log('The User ID changed! Fetching new user data...');
}, [userId]);Common Pitfalls
- The Infinite Loop of Death: If you call a state setter (e.g.,
setCount) inside auseEffectthat has NO dependency array, you create an infinite loop. The effect updates state -> State triggers a re-render -> Re-render triggers the effect -> Effect updates state. Your browser will freeze and crash. Always use a dependency array. - The Stale Closure (Missing Dependencies): If your effect uses a state variable (e.g.
userId), but you FORGET to putuserIdin the dependency array[], your effect will be trapped in a 'stale closure'. It will permanently use the very firstuserIdvalue it saw, completely ignoring future updates. If you use it inside the effect, you MUST list it in the array.
Interview Questions
You pass an empty array [] as the second argument. This tells React that the effect depends on absolutely nothing. Because 'nothing' can never change, React runs it once on the initial mount and never runs it again.
The react-hooks/exhaustive-deps rule strictly enforces that every single prop, state, or function declared inside the component that is used inside the useEffect block must be explicitly listed in the dependency array. This prevents insidious stale closure bugs.
Real-World Example
Syncing with a Specific Prop: If a user is viewing Profile #1, and clicks a link to view Profile #2, the component doesn't unmount. The parent simply passes a new userId prop. By listing userId in the dependency array, React automatically detects the change and re-runs the API fetch for the new user.
function UserProfile({ userId }) {
const [userData, setUserData] = useState(null);
// This effect will run on the very first load,
// AND anytime the parent passes down a NEW userId.
useEffect(() => {
// Only runs when 'userId' changes!
async function fetchNewUser() {
const response = await fetch(`/api/users/${userId}`);
setUserData(await response.json());
}
fetchNewUser();
}, [userId]); // <- The Dependency Array
if (!userData) return <Loading />;
return <div>{userData.name}</div>;
}Check Your Knowledge
Test your understanding of Dependency Array with these quick questions.