AniUI Academy

The Dependency Array

How useEffect's second argument controls when an effect re-runs, why every value the effect reads belongs in it, and the classic stale-closure bug that happens when one is missing.

9 min read

The previous lesson used an effect with no second argument, which runs after every render — rarely what you actually want. The dependency array is how you tell React precisely which renders should re-trigger an effect.

Three distinct behaviors

useEffect(() => {
  console.log("runs after every render");
});
 
useEffect(() => {
  console.log("runs once, after the first render only");
}, []);
 
useEffect(() => {
  console.log("runs after the first render, and again whenever userId changes");
}, [userId]);
  • No array at all — runs after every render, no exceptions.
  • Empty array [] — runs once, after the first render, and never again (until the component unmounts and a new instance mounts).
  • Array with values [a, b] — runs after the first render, and again on any later render where a or b differs from its value in the previous render.

React compares each entry in the array to the same position in the previous render's array, using a comparison similar to Object.is (basically reference equality for objects/arrays/functions, value equality for primitives). If every entry is unchanged, the effect is skipped for that render entirely — this is the whole optimization the array exists to enable.

The rule: include everything the effect reads

The dependency array isn't a list of "things that should cause a re-run" — that's a side effect of what it really is: an honest list of every value from the surrounding scope (props, state, or anything derived from them) that the effect function reads. Leaving one out doesn't stop that value from changing — it just stops the effect from finding out.

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
 
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, []); // BUG: reads userId, but doesn't list it
 
  return <p>{user?.name}</p>;
}

If userId changes because the parent now shows a different user, this effect doesn't re-run — the empty array told React "nothing here ever changes." The effect function closed over the first render's userId and keeps using it forever. This is called a stale closure, and it's the single most common React bug caused by the dependency array — the fix is simply to list what's actually read:

useEffect(() => {
  fetchUser(userId).then(setUser);
}, [userId]); // correct — re-fetches whenever userId changes

Most editor setups for React include an ESLint rule (react-hooks/exhaustive-deps) that flags exactly this mistake by comparing the array against what the effect body actually references — it's worth keeping enabled and treating its warnings seriously rather than silencing them, since a silenced warning here is very often a real bug.

Objects, arrays, and functions as dependencies

A subtler version of the same problem: a dependency that's an object, array, or function created fresh during render is a new reference every single time, even if its contents look identical:

function SearchResults({ query }) {
  const options = { caseSensitive: false }; // new object every render
 
  useEffect(() => {
    runSearch(query, options);
  }, [query, options]); // options "changes" every render — effect runs every time
}

Because options is a brand-new object on every render, the dependency comparison sees a different reference each time and reruns the effect on every render — defeating the entire point of the array. The fix, once you reach the performance part of this course, is usually useMemo (to keep the same object reference across renders when its actual contents haven't changed) — or, simpler here, just listing the primitive fields you actually need instead of the whole object:

useEffect(() => {
  runSearch(query, { caseSensitive: false });
}, [query]); // caseSensitive is a constant, not a dependency at all

Try it yourself

Watch the console (or the effect count below) to see the difference between an effect that depends on a changing value and one that doesn't:

Try it yourself
Loading playground...

What to remember

  • No array: every render. Empty array: once, after the first render. Array with values: after the first render, and again whenever one of those values changes.
  • The array should honestly list every value the effect reads from props/state — it's not a trigger list, it's a dependency list.
  • Omitting a value the effect actually uses causes a stale closure: the effect keeps using that value's first-render snapshot forever.
  • Objects, arrays, and functions created fresh during render are new references every time, which can make a dependency array trigger on every render unless the value is memoized or replaced with its underlying primitives.

Check yourself

4 questions · pass 3/4 to unlock Cleanup Functions in Effects

up to 50
  1. 1.What does passing an empty array [] as useEffect's second argument mean?

  2. 2.How does React decide whether to re-run an effect between renders, given a dependency array like [userId]?

  3. 3.An effect reads userId from props but the dependency array is []. What bug does this cause?

  4. 4.Why do object and array dependencies commonly cause an effect to re-run on every single render, even when nothing meaningful changed?

4 left to answer