Effect Dependencies

List Every Reactive Value the Effect Reads

Effect Dependencies

The dependency array tells React when to re-run an effect. It should list every state, prop, or context value the effect uses.

4 min read Level 3/5 #react#useEffect#dependencies
What you'll learn
  • Pick the right dep array
  • Avoid "exhaustive deps" warnings
  • Recognize stale closures

The dependency array is how React knows when to re-run an effect. The rule: list everything reactive that the effect reads. State, props, context, derived values from any of those.

The Rule

function UserCard({ id }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`/api/users/${id}`).then(r => r.json()).then(setUser);
  }, [id]);   // ← id is read inside, so id must be in deps
}

ESLint’s react-hooks/exhaustive-deps rule will warn if you miss one. Listen to that rule. Suppressing it almost always hides a bug.

What Counts as a Dependency

Anything that’s defined outside the effect but read inside:

  • Props
  • State values
  • Context values
  • Other variables in scope (helpers, derived values, refs’ .current — actually, refs are an exception, see below)

What Does NOT

  • useState setters (setX) — stable across renders, React guarantees it
  • useRef returns (the ref object itself) — stable
  • Globally imported things — stable, not reactive

The Stale Closure Pitfall

Each render captures its own values. If you skip deps, the effect keeps using values from when it last ran:

function Timer({ message }) {
  useEffect(() => {
    const id = setInterval(() => console.log(message), 1000);
    return () => clearInterval(id);
  }, []);   // ← missing `message`
}

When message changes, the interval still logs the original one. The fix is to include message:

useEffect(() => {
  const id = setInterval(() => console.log(message), 1000);
  return () => clearInterval(id);
}, [message]);

Now the interval re-creates each time message changes — the right behavior.

”But I Don’t Want to Re-run!”

If the effect should run only when one specific value changes — not every dep — the dep IS that value. If the effect needs to read something else WITHOUT triggering a re-run, that’s what refs are for (chapter 5).

Functions In The Dep Array

A function defined in the component body is a NEW function every render — so listing it in deps means the effect re-runs every render. Two fixes:

  1. Move the function inside the effect — no longer a dep.
  2. Wrap it in useCallback so its identity is stable.
// Approach 1 — move it inside
useEffect(() => {
  function load() { fetch(...).then(...); }
  load();
}, [id]);

// Approach 2 — useCallback (chapter 5)
const load = useCallback(() => fetch(`/api/${id}`), [id]);
useEffect(() => { load(); }, [load]);

Linter, Then Brain

The ESLint rule isn’t always smart enough — sometimes you need to restructure the effect. But when it says “missing dep”, check first whether you can include it. Suppressing the warning is almost never the answer.

Up Next

Many effects need to undo something on unmount. That’s cleanup.

Cleanup →