ReactMedium
useEffect dependency array in React — the exhaustive rules
The dependency array is React's way of knowing when to re-run an effect. Every external value the effect reads MUST be listed.
The three modes
useEffect(() => { ... }); // After EVERY render — rarely what you want
useEffect(() => { ... }, []); // Once, after mount only
useEffect(() => { ... }, [x]); // Whenever x changes (compared by Object.is)
The rule
List every value from outside the effect that the effect reads. Props, state, derived variables. If you skip one, you have a stale closure bug.
function Search({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
fetch(`/api/search?q=${query}`)
.then((r) => r.json())
.then(setResults);
}, [query]); // ✅ Re-runs when query changes
}
The classic stale-closure bug
function Timer({ delay }) {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // ❌ stale — captures count from first render
}, delay);
return () => clearInterval(id);
}, [delay]); // ❌ missing count
}
// Fix 1 — list count (but then interval resets every tick)
// Fix 2 — functional setter, no dependency on count
useEffect(() => {
const id = setInterval(() => setCount((c) => c + 1), delay);
return () => clearInterval(id);
}, [delay]); // ✅
ESLint rule — turn it on
{
"extends": ["plugin:react-hooks/recommended"]
}
react-hooks/exhaustive-deps will yell at you for missing deps. Listen to it. "Suppressing the warning" is almost always the wrong fix.
When you want to "fix" by removing deps
Don't. Restructure instead.
// ❌ "I only want this to run on mount"
useEffect(() => {
doSomething(userId); // depends on userId, but I don't want re-runs
}, []);
// eslint-disable-next-line react-hooks/exhaustive-deps
// ✅ If you genuinely want mount-only, the dependency probably shouldn't be a
// prop — move logic to a parent that mounts when userId changes:
function Wrapper({ userId }) {
return <Inner key={userId} />; // key change remounts Inner
}
Object / function dependencies
// ❌ options is a new object every render — effect runs every time
useEffect(() => { ... }, [{ id: 1 }]);
// ❌ Same — inline function
useEffect(() => { ... }, [() => {}]);
// ✅ Memoize, or extract the primitive you care about
const optsId = 1;
useEffect(() => { ... }, [optsId]);
For complex objects, useMemo to keep the same reference across renders, or compare by individual keys.
When the dependency is a callback prop
function Child({ onChange }) {
useEffect(() => {
const id = setInterval(() => onChange(Date.now()), 1000);
return () => clearInterval(id);
}, [onChange]); // ✅ — but parent must memoize onChange to avoid resets
}
// Parent
const handleChange = useCallback((t) => setTime(t), []);
return <Child onChange={handleChange} />;
Cheat sheet — picking the dependency array
| You want | Array |
|---|---|
| Run once after mount | [] |
| Run on every render | omit entirely |
| Re-run when X changes | [x] |
| Re-run when X or Y changes | [x, y] |
| Re-run when a deep object changes | extract the primitive you care about and depend on that |