Question presented to candidate: "A long-running React app gets slower the more the user navigates around. What kinds of memory leaks does React make easy to write, and how do you prevent them?"
What a strong answer should cover:
- The single unifying rule: every effect that sets something up must return a cleanup that tears it down.
- The usual culprits: event listeners,
setInterval/setTimeout, WebSocket and observer subscriptions, and in-flight requests. - Cleanup runs before the next effect run and on unmount — that ordering is what makes it work.
AbortControllerfor fetches; a cancellation flag when the API cannot be aborted.- Observers (
IntersectionObserver,ResizeObserver,MutationObserver) need explicitdisconnect(). - A subtle one: a closure captured in a long-lived subscription keeps its entire scope alive, including large objects.
- StrictMode makes this visible in development by mounting, cleaning up, and remounting — an effect that leaks accumulates immediately.
- Modern nuance: since React 18 a
setStateon an unmounted component is a silent no-op, so the old "can't perform a React state update on an unmounted component" warning is gone. Its absence does not mean there is no leak. - Diagnosis: DevTools Profiler for render cost, Chrome Memory panel heap snapshots and the detached-DOM-node check for actual retention.
Clarifying questions expected:
- "Is memory actually growing, or is it a re-render performance problem?" — different diagnosis entirely.
- "Does it get worse with navigation, or over time on one screen?"
Code / implementation expected: Yes — an effect with a cleanup, and the AbortController fetch pattern.