ESC

Type to search the knowledge base.

useEffect Dependency Array

The useEffect dependency array is a contract: Object.is compares, exhaustive-deps, stale closures, and what not to put in effects.

intermediate3 min read
  • react
  • useeffect-dependency

The dependency array tells React when to re-run an effect after commit. React compares each dependency with Object.is to the previous render’s list. Get it wrong and you get stale UI, infinite loops, or silent “works on my machine” bugs.

Docs: useEffect, Lifecycle of Reactive Effects, Removing Effect Dependencies.

The three shapes

Deps Behavior
omitted Re-run after every commit (almost always wrong; linter flags)
[] Run after mount; cleanup on unmount
[a, b] Re-run when a or b fails Object.is equality
useEffect(() => {
  const id = setInterval(() => {
    console.log(count);
  }, 1000);
  return () => clearInterval(id);
}, [count]);

If you omit count from deps but log it, you print a stale count forever (closure from the first render with []).

Exhaustive deps is not pedantry

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

  useEffect(() => {
    let cancelled = false;
    fetch(`/api?q=${encodeURIComponent(query)}`)
      .then((r) => r.json())
      .then((data) => {
        if (!cancelled) setResults(data);
      });
    return () => {
      cancelled = true;
    };
  }, [query]); // query is read → must be listed

  return <List items={results} />;
}

Missing query means the effect never re-fetches. Including a new object every render (options = { q: query } created inline) means it always re-fetches — stabilize with useMemo or pass primitives.

Objects, functions, and identity

// Parent
<Panel onSave={() => save(form)} theme={{ mode }} />

// Child effect depends on onSave / theme → runs every parent render
useEffect(() => {
  bridge.register(onSave);
}, [onSave]);

Stabilize with useCallback / useMemo, or better: do not put the callback in an effect if an event handler can own the logic (You Might Not Need an Effect).

Infinite loops

useEffect(() => {
  setUser({ ...user, checked: true });
}, [user]); // new object every time → loop

Fix by depending on user.id, computing during render, or structuring updates so you do not write the same dependency you recreate.

Server and Strict Mode

Effects do not run on the server. In development Strict Mode, React mounts, unmounts, and remounts to surface missing cleanup — your effect may run twice. Cleanups must be real (Strict Mode double effects).

Interview out-loud

“Dependencies list every reactive value the effect reads. React re-runs the effect when Object.is says a dep changed, after running the previous cleanup. Stale closures come from missing deps; loops come from unstable object deps that you also setState into. Prefer event handlers over effects when the trigger is a user action.”

Further reading

Production checklist

Before you ship a change in this area, walk the list out loud:

  1. What is the source of truth for the data on screen?
  2. What happens on remount, route change, and Strict Mode double-invoke?
  3. Which updates are urgent (input) versus deferrable (filter large lists)?
  4. Did you profile before adding memo, context splits, or virtualization?
  5. Is there an accessibility path: keyboard, focus, names, and errors?

If you cannot answer those, the API knowledge will not save the interview or the incident.

Related guides