
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:
- Add a comment explaining why
- Ensure nothing you’re using can go stale
- Consider
useRefas 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 








