useEffect Dependency Array
The useEffect dependency array is a contract: Object.is compares, exhaustive-deps, stale closures, and what not to put in effects.
- react
- useeffect-dependency
The dependency array tells React when to re-run an effect after commit. React compares each dependency with Object.is to the previous render’s list. Get it wrong and you get stale UI, infinite loops, or silent “works on my machine” bugs.
Docs: useEffect, Lifecycle of Reactive Effects, Removing Effect Dependencies.
The three shapes
| Deps | Behavior |
|---|---|
| omitted | Re-run after every commit (almost always wrong; linter flags) |
[] |
Run after mount; cleanup on unmount |
[a, b] |
Re-run when a or b fails Object.is equality |
useEffect(() => {
const id = setInterval(() => {
console.log(count);
}, 1000);
return () => clearInterval(id);
}, [count]);
If you omit count from deps but log it, you print a stale count forever (closure from the first render with []).
Exhaustive deps is not pedantry
function Search({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
let cancelled = false;
fetch(`/api?q=${encodeURIComponent(query)}`)
.then((r) => r.json())
.then((data) => {
if (!cancelled) setResults(data);
});
return () => {
cancelled = true;
};
}, [query]); // query is read → must be listed
return <List items={results} />;
}
Missing query means the effect never re-fetches. Including a new object every render (options = { q: query } created inline) means it always re-fetches — stabilize with useMemo or pass primitives.
Objects, functions, and identity
// Parent
<Panel onSave={() => save(form)} theme={{ mode }} />
// Child effect depends on onSave / theme → runs every parent render
useEffect(() => {
bridge.register(onSave);
}, [onSave]);
Stabilize with useCallback / useMemo, or better: do not put the callback in an effect if an event handler can own the logic (You Might Not Need an Effect).
Infinite loops
useEffect(() => {
setUser({ ...user, checked: true });
}, [user]); // new object every time → loop
Fix by depending on user.id, computing during render, or structuring updates so you do not write the same dependency you recreate.
Server and Strict Mode
Effects do not run on the server. In development Strict Mode, React mounts, unmounts, and remounts to surface missing cleanup — your effect may run twice. Cleanups must be real (Strict Mode double effects).
Interview out-loud
“Dependencies list every reactive value the effect reads. React re-runs the effect when Object.is says a dep changed, after running the previous cleanup. Stale closures come from missing deps; loops come from unstable object deps that you also setState into. Prefer event handlers over effects when the trigger is a user action.”
Related on this site
Further reading
Production checklist
Before you ship a change in this area, walk the list out loud:
- What is the source of truth for the data on screen?
- What happens on remount, route change, and Strict Mode double-invoke?
- Which updates are urgent (input) versus deferrable (filter large lists)?
- Did you profile before adding memo, context splits, or virtualization?
- Is there an accessibility path: keyboard, focus, names, and errors?
If you cannot answer those, the API knowledge will not save the interview or the incident.
Related guides
- Accessibility Patterns in ReactPractical React a11y: labels, focus management, keyboard, live regions, and composition patterns that stay accessible.
- Avoid Prop Drilling with CompositionStop threading props through intermediates: children slots, inversion of control, and when context is the right escape hatch.
- Batching State UpdatesHow React 18+ batches setState in events, timeouts, and promises: when updates flush and why double setState still works.
- Children Prop PatternsUsing children and slot props for flexible APIs: wrappers, compound components, and when to prefer explicit props.
- Client Component BoundariesWhere to put use client: push interactivity to leaves, serializable props, and children as server slots.