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.
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
useStatesetters (setX) — stable across renders, React guarantees ituseRefreturns (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:
- Move the function inside the effect — no longer a dep.
- Wrap it in
useCallbackso 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 →