Memory leaks in React are a common interview topic because they affect performance, memory usage, and application stability. A memory leak occurs when objects that are no longer needed remain in memory because something still references them.
What is a Memory Leak?
A memory leak happens when memory allocated for an object cannot be released because it is still being referenced, even though it is no longer needed.
Example
Imagine a user visits a page.
- React creates components.
- Components allocate memory.
- User navigates away.
- Component is removed from the UI.
If some timer, event listener, or API request is still running and holding a reference to the component, JavaScript cannot garbage collect it.
Result:
- Increasing memory usage
- Slow application
- High CPU usage
- Browser crashes (large applications)
How Memory Normally Works
| Lifecycle Stage | Without Memory Leak ✅ | With Memory Leak ❌ |
|---|---|---|
| Component Mount | React creates the component and allocates memory. | React creates the component and allocates memory. |
| Side Effects | Resources (timer, listener, API, WebSocket) are created. | Resources (timer, listener, API, WebSocket) are created. |
| User Navigates Away | Component unmounts. | Component unmounts. |
| Cleanup | Cleanup function removes all resources. | Cleanup function is missing or incomplete. |
| References | No references remain. | Active references still exist. |
| Garbage Collection | JavaScript Garbage Collector releases memory. | Garbage Collector cannot release memory because references still exist. |
| Final Result | Stable memory usage and good performance. | Increasing memory usage, slower application, and potential browser crashes. |
Common Memory Leaks in React
| Memory Leak | Problem | Solution |
|---|---|---|
Timer (setInterval) | Continues running after component unmount | clearInterval() |
| Event Listener | Listener remains attached | removeEventListener() |
| Fetch Request | Response arrives after component unmount | AbortController.abort() |
| WebSocket | Connection remains open | socket.close() |
| Subscription (RxJS) | Subscription continues receiving updates | unsubscribe() |
| Async Operation | Updates state after component unmount | Cancel request or ignore result |
| Large Closure | Large objects stay referenced | Remove unnecessary references |
1. Timer (setInterval)
Problem
A timer continues executing even after the component has been removed from the DOM.
Not Recommended
useEffect(() => {
const id = setInterval(() => {
console.log("Running...");
}, 1000);
}, []);
Why is this bad?
- Interval never stops.
- JavaScript still keeps a reference.
- Memory cannot be reclaimed.
✅ Recommended
useEffect(() => {
const id = setInterval(() => {
console.log("Running...");
}, 1000);
return () => {
clearInterval(id);
};
}, []);
Why is this recommended?
- Interval is cleared during cleanup.
- No unnecessary work continues.
- Memory can be released.
2. Event Listener
Problem
Every mount adds a new event listener.
Not Recommended
useEffect(() => {
window.addEventListener("resize", handleResize);
}, []);
Why is this bad?
- Listener remains attached.
- Multiple listeners accumulate.
- Causes memory leaks and duplicate executions.
✅ Recommended
useEffect(() => {
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
Why is this recommended?
- Removes listener when component unmounts.
- Prevents duplicate listeners.
- Frees memory.
3. Fetch API Request
Problem
The API request completes after the component has already unmounted.
Not Recommended
useEffect(() => {
fetch("/users")
.then(res => res.json())
.then(setUsers);
}, []);
Why is this bad?
- Request continues running.
- May try to update state after unmount.
- Wastes network and memory resources.
✅ Recommended
useEffect(() => {
const controller = new AbortController();
fetch("/users", {
signal: controller.signal
})
.then(res => res.json())
.then(setUsers);
return () => {
controller.abort();
};
}, []);
Why is this recommended?
- Cancels pending request.
- Prevents unnecessary work.
- Avoids state updates after unmount.
4. WebSocket
Problem
Connection stays open forever.
Not Recommended
useEffect(() => {
const socket = new WebSocket(url);
}, []);
Why is this bad?
- Open connection consumes memory.
- Browser keeps network resources allocated.
✅ Recommended
useEffect(() => {
const socket = new WebSocket(url);
return () => {
socket.close();
};
}, []);
Why is this recommended?
- Closes the connection.
- Releases network resources.
- Prevents memory leaks.
5. RxJS Subscription
Problem
Subscription continues after component removal.
Not Recommended
useEffect(() => {
observable.subscribe(data => {
setData(data);
});
}, []);
Why is this bad?
- Component keeps receiving updates.
- Memory usage keeps growing.
✅ Recommended
useEffect(() => {
const subscription = observable.subscribe(data => {
setData(data);
});
return () => {
subscription.unsubscribe();
};
}, []);
Why is this recommended?
- Stops receiving updates.
- Releases subscription resources.
6. Async Operation
Problem
Async function finishes after component unmount.
Not Recommended
useEffect(() => {
async function load() {
const data = await fetchData();
setData(data);
}
load();
}, []);
Why is this bad?
- State update occurs after component removal.
- Creates unnecessary work.
✅ Recommended
useEffect(() => {
let active = true;
async function load() {
const data = await fetchData();
if (active) {
setData(data);
}
}
load();
return () => {
active = false;
};
}, []);
Why is this recommended?
- Ignores completed requests after unmount.
- Prevents invalid state updates.
7. Large Closures
Problem
A callback captures a huge object.
Not Recommended
const hugeArray = new Array(1000000);
function handleClick() {
console.log(hugeArray.length);
}
Why is this bad?
- Callback keeps a reference to a large object.
- Garbage collector cannot free it while the callback is still referenced.
✅ Recommended
// Keep only the data you need.
const count = hugeArray.length;
function handleClick() {
console.log(count);
}
// Also remove callbacks/listeners
// when they are no longer required.
Why is this recommended?
- Reduces retained memory.
- Allows large objects to be garbage collected sooner.
Best Practices Checklist
| ✔ Do | ❌ Don’t |
|---|---|
Return a cleanup function from useEffect | Forget cleanup |
| Clear timers | Leave intervals running |
| Remove event listeners | Keep adding listeners |
| Abort fetch requests | Ignore pending requests |
| Close WebSockets | Leave connections open |
| Unsubscribe from subscriptions | Keep subscriptions alive |
| Disconnect observers | Leave observers active |
Always remember
Performance Problems
Performance problems make your application slower, but they do not necessarily increase memory usage.
- Unnecessary Re-renders
- Components render more often than needed.
- Example: Parent component re-renders causing all child components to re-render.
- Expensive Calculations
- Heavy computations execute on every render.
- Example: Sorting or filtering a large dataset repeatedly.
- Large DOM Updates
- React updates many DOM elements unnecessarily.
- Example: Re-rendering an entire table when only one row changes.
- Poor Memoization
- Functions or computed values are recreated on every render.
- Example: Not using
useMemo,useCallback, orReact.memowhere appropriate.
Memory Problems (Memory Leaks)
Memory problems occur when objects remain in memory even after they are no longer needed, causing memory usage to grow over time.
- Uncleared Timers
setInterval()orsetTimeout()continues running after the component unmounts.- Solution: Use
clearInterval()orclearTimeout()in the cleanup function.
- Unremoved Event Listeners
- Event listeners remain attached to the window or DOM elements.
- Solution: Call
removeEventListener()during cleanup.
- Open WebSockets
- WebSocket connections stay open after leaving the page.
- Solution: Close the connection using
socket.close().
- Active Subscriptions
- Observables or subscriptions continue receiving data after component unmount.
- Solution: Call
unsubscribe()in the cleanup function.
- Uncancelled Async Operations
- API requests complete after the component has been removed.
- Solution: Cancel requests using
AbortControlleror ignore the response.
- Retained References
- Closures, callbacks, or global variables continue referencing objects that are no longer needed.
- Solution: Remove unnecessary references so the JavaScript Garbage Collector can reclaim memory.
