Closures are not a React smell. They are normal JavaScript. They become a memory problem only when some other system keeps a callback alive after the component that created it should have disappeared.
That is why the same bugs keep returning in different forms:
- event listeners
- intervals
- subscriptions
- observers
A Typical Leak Shape
The problem usually looks like this:
useEffect(() => {
function onResize() {
setWidth(window.innerWidth);
}
window.addEventListener("resize", onResize);
}, []);
If the listener is never removed, the callback can keep references to component state and updater functions longer than intended. One leak may be small. Repeated navigation through the same screen can turn it into a steady climb in memory usage.
The correct pattern keeps registration and cleanup together:
useEffect(() => {
function onResize() {
setWidth(window.innerWidth);
}
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, []);
What To Inspect in Real Apps
When memory grows after route changes or long sessions, start with long-lived callbacks that capture component state. The question is simple: who still holds this closure?
That investigation usually leads to:
- browser listeners
- library subscriptions
- unclosed observers
- custom event buses
Better Engineering Rule
Whenever an effect registers a callback outside React, write the cleanup path in the same effect before you move on. That habit prevents more leaks than any later profiling session.
Further Reading