How do you handle memory leaks in React apps
Pair every subscription with cleanup: `useEffect` returns a teardown; remove listeners; clear intervals; abort fetches with AbortController; unsubscribe from stores. Avoid holding refs to large objects in long-lived closures. Bound caches (LRU). Detect with DevTools Memory snapshots (detached DOM, retained size). Strict Mode helps surface missing cleanup in dev.
Common leak sources
| Source | Fix |
|---|---|
addEventListener without remove | Cleanup in useEffect return; or AbortController signal. |
setInterval / setTimeout not cleared | clearInterval/Timeout in cleanup. |
| Store subscriptions (Zustand, Redux observers) | Unsubscribe in cleanup. |
| In-flight fetch after unmount | AbortController signal. |
| Detached DOM kept by JS refs | Null refs in cleanup. |
| Unbounded caches / Maps | LRU with cap. |
| Long-lived closures over big data | Drop refs after use. |
| Server-injected state captured in module scope | Initialize per-request in SSR; don't share across requests. |
The standard cleanup shape
useEffect(() => {
const ac = new AbortController();
window.addEventListener("resize", onResize, { signal: ac.signal });
const id = setInterval(tick, 1000);
const unsub = store.subscribe(onChange);
return () => {
ac.abort();
clearInterval(id);
unsub();
};
}, []);One AbortController can cover many listeners.
Aborting fetch on unmount
useEffect(() => {
const ac = new AbortController();
fetch("/data", { signal: ac.signal })
.then((r) => r.json())
.then(setData)
.catch((e) => { if (e.name !== "AbortError") setError(e); });
return () => ac.abort();
}, []);React Query / SWR
Library handles cancellation + dedup + cleanup for you. Strongly prefer over hand-rolled fetch + useState for server state.
Strict Mode
In React 18+, dev-only Strict Mode mounts/unmounts/remounts every component twice. Reveals missing cleanups because effects run, clean up, run again. If you see double API calls in dev only, the fix isn't to "disable strict mode" — it's to add cleanup so the second run is benign.
Detached DOM
A node removed from the DOM but still referenced from JS can't be GC'd. Symptoms: heap growth after navigation. Find via Memory panel → "Detached".
// BAD — closure holds a node ref forever
const ref = useRef();
useEffect(() => {
const node = ref.current;
window.heavyHandler = () => doStuff(node); // global retention
}, []);Avoid global retention; null out refs in cleanup if holding heavy nodes.
Unbounded caches
const cache = new Map(); // grows foreverBound with LRU + cap or TTL. Or use WeakMap when key objects can be GC'd.
Detection workflow
- Reproduce in dev with Strict Mode on.
- Memory panel — heap snapshot, navigate, snapshot again, compare. Look at "Detached HTMLElement" and retainers.
- Performance Monitor (DevTools) — watch JS heap size during prolonged interaction.
- Reproduce a leak loop — open/close a modal 50 times; if heap grows linearly, leak.
- In production — track
performance.memory.usedJSHeapSize(Chrome) trends.
Architecture-level
- Long-lived SPA (Gmail, Slack) — more leak surface area.
- Page-per-route SSR — heap reset on every nav.
- Background tabs — Chrome discards eventually; recovery via session restore.
Interview framing
"Pair every subscription with cleanup. useEffect return is the natural place — clearInterval, removeEventListener (or AbortController signal), unsubscribe, abort in-flight fetches. AbortController is the cleanest pattern because one signal can clean up many listeners + fetches. For server state use React Query — it handles cancellation and dedup. Strict Mode in dev surfaces missing cleanup by double-mounting. Detect leaks with DevTools Memory panel heap snapshots + detached DOM report. Watch for unbounded caches and long-lived closures over big objects — bound with LRU or WeakMap."
Follow-up questions
- •Walk through finding a leak with DevTools.
- •Why does Strict Mode double-mount in dev?
- •How does AbortController integrate with React?
Common mistakes
- •Missing cleanup in useEffect.
- •Global retention of node refs.
- •Unbounded caches.
- •Ignoring Strict Mode warnings.
Performance considerations
- •Heap growth → eventual crash on mobile. Long GC pauses precede the crash; watch p75 INP.
Edge cases
- •WebSocket / SSE reconnection leaks.
- •Cycles between React state and external library.
- •Workers not terminated.
Real-world examples
- •Long-running SPAs, dashboard apps, editor apps.