Back to Articles
Programming Feb 6, 2026 · 10 min read

How I Solved a Tricky Memory Leak in React useEffect

Daniel

Software Developer

Last week, one of our production dashboards started slowing down after about 20 minutes of use. It wasn’t a sudden crash, but a slow, painful death of responsiveness. Here is how I tracked it down to a single useEffect hook.

The Symptoms

Users reported that switching between tabs in our analytics dashboard eventually made the browser unresponsive. A quick look at the Chrome Task Manager showed memory usage climbing steadily: 200MB, 500MB, 1.2GB…

The Investigation

I used the Chrome Performance Profiler to record a session. The heap snapshot revealed thousands of detached DOM nodes and event listeners that weren’t being garbage collected.

The Culprit

I narrowed it down to a real-time data widget component. Here is the simplified problematic code:

useEffect(() => {
const connection = createWebSocketConnection();
connection.on('data', (data) => {
setData(data);
});
}, []);

See the issue? The connection was established, but never closed when the component unmounted. Every time the user navigated away and back, a NEW connection was created, leaving the old one alive and holding onto references.

The Fix

The solution was simple but crucial: adding a cleanup function.

useEffect(() => {
const connection = createWebSocketConnection();
connection.on('data', (data) => {
setData(data);
});

// The Cleanup Function
return () => {
connection.disconnect();
};
}, []);

Lessons Learned

1. Always think about lifecycle: When you create a subscription, timer, or listener in useEffect, immediately write the cleanup return.

2. Tools are your friend: The Memory tab in Chrome DevTools is intimidating but essential for these kinds of bugs.

3. SPA routing: In Single Page Applications, “navigating away” doesn’t reset the page state. Components mount and unmount, and your code needs to handle that gracefully.