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
- useCopyToClipboard HookCopy text with navigator.clipboard, fallback execCommand, transient copied state, and permissions that fail in insecure contexts.
- useDebouncedValue HookDebounce a changing value in React without firing effects on every keystroke. Cleanup, leading vs trailing, and the stale timer that updates unmounted state.
- useEventListener HookAttach a window, document, or element listener with a stable handler ref so you do not rebind on every render. Capture option and target changes.
- useFetch With AbortControllerFetch in an effect with a signal, ignore aborted errors, and prevent the slow response from winning a race after the url changes.
- useIdle HookDetect idle users: reset a timer on input events, fire idle after timeout, and decide whether mount starts the timer immediately.