5 min read
0%

Dependency Array Pitfalls

Back to Blog
Dependency Array Pitfalls

Dependency Array Pitfalls

The dependency arrays in useEffect, useCallback, and useMemo are a common source of subtle bugs. Missing values cause stale closures; unnecessary values cause too many re-runs.

The Rule

Every value from the component scope used inside the hook must be in the dependency array. The react-hooks/exhaustive-deps ESLint rule catches most violations automatically — treat its warnings as errors.

Object and Array Dependencies

Objects and arrays are new references on every render:

useEffect(() => {
  fetchUser(options);
}, [options]); // re-runs every render — options is always a new object

Fix: destructure to primitives:

const { id, role } = options;
useEffect(() => {
  fetchUser({ id, role });
}, [id, role]); // stable primitives

Function Dependencies

Inline functions are new references every render:

useEffect(() => {
  subscribe(onData); // onData is new every render
}, [onData]); // infinite loop if subscribe triggers re-render

Fix with useCallback:

const onData = useCallback((data) => {
  setResults(data);
}, []);

useEffect(() => {
  subscribe(onData);
  return () => unsubscribe(onData);
}, [onData]);

Missing Dependencies

useEffect(() => {
  document.title = `${count} notifications`; // count not in deps
}, []); // title never updates when count changes

Add count to the dep array, or if you genuinely want fire-once behavior, make sure the effect body doesn’t need the current value (use a ref instead).

The Empty Array Misconception

[] doesn’t mean “run once when convenient” — it means “this effect has zero dependencies and will never need to re-run because values changed.” If the effect body uses any component scope values, they’re missing from deps.

Primitive Extraction Pattern

// Instead of:
useEffect(() => { ... }, [user]); // user is always a new object

// Extract what you actually need:
const userId = user.id;
const userName = user.name;
useEffect(() => { ... }, [userId, userName]);

When to Suppress the Lint Rule

Rarely. When you do suppress exhaustive-deps:

  1. Add a comment explaining why
  2. Ensure nothing you’re using can go stale
  3. Consider useRef as a safer alternative
useEffect(() => {
  // Intentional: fires once on mount to initialize analytics
  analyticsInit(config);
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

useEffectEvent (Experimental)

React is developing useEffectEvent for callbacks that need current values but shouldn’t be in deps:

const onTick = useEffectEvent(() => {
  console.log(count); // always current, not a dep
});

useEffect(() => {
  const id = setInterval(onTick, 1000);
  return () => clearInterval(id);
}, []); // no count dep needed

Canvas is not supported in your browser