Why React's Dependency Array is Not About Dependencies

Apr 8, 2022

Let's talk about React's dependency array—the second argument of hooks like useEffect, useCallback, and useMemo. Despite its name, this array isn't really about dependencies at all.

Wait, what?

That's right. Let's unpack that.

What React Documentation Tells You

React docs introduce the dependency array as a list of variables that, when changed, trigger re-execution of the hook:

useEffect(() => {
  console.log(value);
}, [value]);

We're taught to think: "This effect depends on value, so value must be in the dependency array." While that's technically accurate, it oversimplifies the mental model, causing confusion.

The Real Purpose of the Dependency Array

The dependency array isn't primarily about listing dependencies. It's about capturing a snapshot of your values from a specific render.

Every render of a React component creates a new closure around the variables and functions within it. When you use the dependency array, you're explicitly telling React, "Give me the fresh version of these variables."

In other words, the dependency array controls the freshness of the closure, not just triggering re-runs.

A Subtle but Crucial Difference

This distinction matters when dealing with complex cases, like asynchronous calls or state updates:

useEffect(() => {
  const handleResize = () => {
    console.log(width);
  };

  window.addEventListener('resize', handleResize);
  return () => window.removeEventListener('resize', handleResize);
}, [width]);

At first glance, it looks like handleResize "depends" on width. But what if you didn't want the event listener to constantly update?

Understanding that the dependency array captures freshness explains why not having width in the dependency array causes a stale closure. You're always referencing the initial value captured on mount.

"Freshness" and Referential Equality

Think of dependency arrays as controlling referential equality rather than "change detection":

[value] doesn't say "run whenever value changes."

Instead, it says, "When the reference of value changes, update the closure to reflect the new reference."

Misunderstanding this often leads to confusion around why objects and functions, which often break referential equality on every render, cause frequent hook re-executions.

How to Think Differently

Stop thinking, "What does this hook depend on?" and start asking:

  • Which values must be fresh each time this hook runs?
  • When do I want to update my closure?

This subtle shift makes handling more complex React logic much clearer:

  • Values that change often but aren't needed fresh can be safely omitted if intentional (use cautiously).
  • Values used in asynchronous functions should always be included if you want their latest snapshot.

Conclusion

The dependency array isn't a list of dependencies in the classic sense—it's a freshness boundary. Treating it this way will simplify your mental model, clarify your code, and avoid subtle bugs related to stale closures.

React isn't magic, just closures and fresh snapshots—embrace that clarity.