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.