useEffect: Dependencies, Cleanup, and Common Traps
When effects run, how dependency arrays work, and how to avoid stale closures.
Frontend Interview Team
February 08, 2026
30‑second interview answer
useEffect lets you run side effects after React commits updates to the DOM—things like data fetching, subscriptions, timers, and manual DOM work. The dependency array controls when the effect runs. Cleanup runs before the next effect (and on unmount) to avoid leaks. Most bugs come from incorrect dependencies and stale closures.
When does useEffect run?
Typical mental model:
- Render happens (React calls components)
- Commit happens (DOM updates)
- Then effects run (after paint, generally)
So useEffect is the right place for work that should not block rendering.
useLayoutEffect vs useEffect
useLayoutEffect: runs after DOM mutations but before paint (can block paint)useEffect: runs after paint (better for most cases)
In interviews:
“Prefer
useEffectunless you must measure layout before paint.”
Dependency array: what it really means
1) No dependency array
useEffect(() => {
// runs after every commit
});2) Empty dependency array []
useEffect(() => {
// run once on mount
return () => {
// cleanup on unmount
};
}, []);3) With dependencies
useEffect(() => {
// runs when any dependency changes
}, [userId, token]);Rule: your deps should include everything from render scope that the effect reads.
Cleanup (critical for interviews)
Cleanup runs:
- before the effect re-runs
- when the component unmounts
Typical patterns:
Subscriptions
useEffect(() => {
const unsub = store.subscribe(() => {
// ...
});
return () => unsub();
}, []);Timers
useEffect(() => {
const id = setInterval(() => {
// ...
}, 1000);
return () => clearInterval(id);
}, []);The most common bug: stale closures
This happens when your effect “captures” old state/props.
Example:
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
console.log(count); // ❌ stale value
}, 1000);
return () => clearInterval(id);
}, []); // ❌ count missing
}Fix 1: include dependencies
useEffect(() => {
const id = setInterval(() => console.log(count), 1000);
return () => clearInterval(id);
}, [count]);Fix 2: use functional updates / refs
useEffect(() => {
const id = setInterval(() => {
setCount((c) => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);Or store mutable latest value in a ref:
const latest = useRef(count);
useEffect(() => {
latest.current = count;
}, [count]);Fetching data safely
Basic fetch
useEffect(() => {
let cancelled = false;
(async () => {
const res = await fetch(`/api/user/${userId}`);
const data = await res.json();
if (!cancelled) setUser(data);
})();
return () => {
cancelled = true;
};
}, [userId]);Better: use AbortController
useEffect(() => {
const controller = new AbortController();
(async () => {
try {
const res = await fetch(url, { signal: controller.signal });
const data = await res.json();
setData(data);
} catch (e) {
if (e.name !== 'AbortError') throw e;
}
})();
return () => controller.abort();
}, [url]);React 18 Strict Mode (why effects run twice in dev)
In development, React may intentionally run certain lifecycles twice to help you find unsafe effects.
Interview-ready line:
“In dev Strict Mode, effects can mount/unmount twice. Write effects to be resilient and clean up properly.”
Anti-patterns to avoid
- Putting async directly in effect callback
useEffect(async () => {}) // ❌Do this instead:
useEffect(() => {
async function run() {}
run();
}, []);- Disabling dependency lint rule blindly
If ESLint says you need deps, you usually do.
- Doing heavy work in effects on every keystroke
Debounce or move work elsewhere.
Mini Q&A
Q1: When does cleanup run?
- Before next run and on unmount.
Q2: Why does missing deps cause bugs?
- The effect closes over old values.
Q3: When to use useLayoutEffect?
- When you must measure layout before paint.
Summary checklist
- I can explain dependency array meaning.
- I know cleanup timing.
- I can explain stale closure with an example.
- I understand Strict Mode double-invoke in dev.