Fix Memory Leaks in React Apps
Memory leaks are a common but often overlooked issue that can degrade the performance and stability of React applications. They occur when components continue to hold references to unused objects, preventing garbage collection and leading to increased memory usage over time.
In React, leaks typically arise from uncleaned side effects such as timers, event listeners, network requests, or subscriptions that persist after a component unmounts. While React’s lifecycle and hooks simplify resource management, overlooking proper cleanup can cause gradual slowdowns or browser crashes.
This article provides a guide to identifying, diagnosing, and fixing memory leaks in React applications.
1. What Are Memory Leaks in React?
A memory leak happens when an application retains references to objects that are no longer needed, preventing the JavaScript engine from reclaiming that memory. In React apps this commonly occurs when a component creates external side-effects (timers, subscriptions, network requests, DOM nodes, WebSocket connections, etc.) but fails to stop or detach them when the component unmounts. The retained resources accumulate over time as components mount and unmount, causing memory to grow even though the visible UI no longer needs those resources.
In practical terms, if a component starts a setInterval and never clears it, the interval callback continues running and may keep references to component state and DOM, preventing garbage collection. Similarly, long-lived subscriptions or unresolved promises referencing component state can leak.
2. How to Detect Memory Leaks
Before addressing a memory leak, the first step is to determine whether your React application is truly affected. Memory leaks can accumulate gradually, making them difficult to spot until performance noticeably degrades. Detecting them early helps ensure your app remains stable and responsive.
Recognizing Early Warning Signs
Some clear indicators can suggest that your React application is leaking memory:
- Gradually Increasing Memory Usage: Your app’s memory consumption may keep rising during normal operation without stabilizing or decreasing, which often indicates that some resources are not being released properly.
- Reduced Performance Over Time: A memory leak can cause your application to feel progressively slower, with delayed rendering, sluggish UI updates, or longer loading times as sessions continue.
- Unexpected Freezes or Crashes: Severe memory leaks can cause your app or browser tab to freeze or crash, especially after prolonged use. These issues often occur when the system runs out of available memory.
Techniques for Detecting Leaks
We can detect memory leaks in React applications by combining browser-based tools with React’s built-in debugging utilities:
- Monitor Memory Usage with Chrome DevTools: Open Chrome DevTools > Performance or Memory tab to observe your application’s memory graph. Watch for steadily increasing memory usage as you interact with different parts of your app.
- Take and Compare Heap Snapshots: Capture heap snapshots at various stages, such as before and after mounting or unmounting components, to track how memory usage changes. Then, compare these snapshots to identify retained detached DOM nodes, event listeners, or closures that continue to hold references even after components are destroyed.
- Use the Browser’s Task Manager: Go to Chrome > More Tools > Task Manager to monitor your application’s memory consumption in real time. A continuous rise in memory usage while performing simple interactions may point to a potential leak.
- Leverage the React Developer Tools: The React Developer Tools extension provides valuable insights into component hierarchies and render behavior. Use the Components tab to check if components remain mounted when they should unmount, and the Profiler tab to detect unnecessary re-renders or components holding large state trees. Combined with Chrome DevTools’ memory analysis, this gives a clearer picture of where leaks are occurring.
- Leverage the React Profiler: The React Profiler helps identify components that re-render excessively or hold onto large trees of elements. Persistent re-renders and retained component trees may point to improper cleanup or inefficient state handling.
Analyzing with Chrome DevTools
Once these symptoms are observed, the next step is to validate the leak using Chrome DevTools. The DevTools Memory panel allows us to record heap snapshots, track object allocations, and analyze retained memory. By taking successive heap snapshots before and after interacting with our application, we can compare them to see whether objects and DOM nodes are being properly released. To get started:
- Open Chrome DevTools and navigate to the Memory tab.
- Select Heap snapshot or Allocations on timeline.
- Interact with your app (mounting, unmounting components, navigating pages).
- Capture snapshots and compare them for steadily increasing retained objects.
A consistent upward trend in retained memory, even after components unmount, confirms that the application is leaking memory. The next step is to use the Retainers view within DevTools to trace which closures, event listeners, or intervals are preventing garbage collection.
3. Common Causes of Memory Leaks with Fixes
Memory leaks typically occur when a component retains references to data, DOM nodes, or asynchronous processes that persist after the component unmounts. Below are some common causes of memory leaks in React applications.
3.1 Timers and Intervals (setTimeout / setInterval)
Timers and intervals are a frequent source of memory leaks, especially when they continue running after a component has been removed from the DOM. When we forget to clear an interval or timeout, the callback function retains references to component state or props, preventing garbage collection.
Problem Example – Leaking Timer
import React, { useState, useEffect } from "react";
function TimerComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(c => c + 1);
}, 1000);
// Missing cleanup – interval continues after unmount
}, []);
return <p>Count: {count}</p>;
}
export default TimerComponent;
In this example, the setInterval function keeps executing even after the component unmounts, holding references to setCount and state values.
Fixed Version – Clean Up the Interval
import React, { useState, useEffect } from "react";
function TimerComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => {
clearInterval(interval); // Proper cleanup
};
}, []);
return <p>Count: {count}</p>;
}
export default TimerComponent;
By returning a cleanup function from useEffect, the interval is cleared when the component unmounts, releasing references and preventing the leak.
3.2 Event Listeners on window, document, or other DOM nodes
Event listeners can also cause memory leaks when they remain attached to global objects such as window, document, or DOM nodes. If not properly removed, these listeners keep the component’s state and functions in memory.
Unremoved Event Listener
import React, { useEffect, useState } from "react";
function ResizeTracker() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener("resize", handleResize);
// No cleanup, event listener persists after unmount
}, []);
return <p>Window width: {width}</p>;
}
export default ResizeTracker;
Here, the resize listener continues to exist even after the component unmounts, leading to memory leaks and unnecessary re-renders.
Remove the Listener
import React, { useEffect, useState } from "react";
function ResizeTracker() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize); // Cleanup
};
}, []);
return <p>Window width: {width}</p>;
}
export default ResizeTracker;
Cleaning up event listeners during component unmount ensures they don’t persist beyond their lifecycle, preventing retained memory references.
3.3 Unresolved or Long-Lived Network Requests (Promises / Fetch)
When a component makes a network request and unmounts before the request resolves, the callback may still attempt to update state. This keeps references alive unnecessarily and may even trigger React warnings about updating unmounted components.
Network Request Without Abort
import React, { useState, useEffect } from "react";
function DataFetcher() {
const [data, setData] = useState(null);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/posts")
.then(res => res.json())
.then(result => setData(result))
.catch(console.error);
// No cancellation, fetch continues even after unmount
}, []);
return <div>{data ? "Data loaded" : "Loading..."}</div>;
}
export default DataFetcher;
If the component unmounts before the fetch resolves, the callback still holds memory references and may attempt to update state.
Abort the Fetch Request
import React, { useState, useEffect } from "react";
function DataFetcher() {
const [data, setData] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch("https://jsonplaceholder.typicode.com/posts", {signal: controller.signal})
.then(res => res.json())
.then(result => setData(result))
.catch(err => {
if (err.name !== "AbortError")
console.error(err);
});
return () => {
controller.abort(); // Cancel request on unmount
};
}, []);
return <div>{data ? "Data loaded" : "Loading..."}</div>;
}
export default DataFetcher;
Using an AbortController ensures that pending network requests are cancelled when the component unmounts, freeing associated memory.
3.4 Managing Refs and Preventing Memory Retention
Refs are useful tools in React for accessing DOM elements or storing mutable values that persist across renders. However, if a ref holds a reference to a large DOM node or component instance and is not cleared on unmount, it can prevent garbage collection and cause a memory leak.
Persistent Ref Reference
import React, { useRef, useEffect } from "react";
function VideoPlayer() {
const videoRef = useRef(null);
useEffect(() => {
videoRef.current = document.createElement("video");
videoRef.current.src = "/sample-video.mp4";
videoRef.current.play();
// videoRef keeps reference even after unmount
}, []);
return <div>Video playing...</div>;
}
export default VideoPlayer;
In this example, the videoRef retains a reference to the video element even after the component is removed from the DOM. Because there’s no cleanup logic, the video element continues to exist in memory, consuming resources unnecessarily and preventing garbage collection.
Clear the Ref on Unmount
To avoid this issue, always release the reference and stop any ongoing activity, such as video playback, during the cleanup phase. This ensures that the referenced element is properly removed and can be garbage-collected when no longer in use.
import React, { useRef, useEffect } from "react";
function SafeVideoPlayer() {
const videoRef = useRef(null);
useEffect(() => {
videoRef.current = document.createElement("video");
videoRef.current.src = "/sample-video.mp4";
videoRef.current.play();
return () => {
videoRef.current.pause();
videoRef.current.src = "";
videoRef.current.load();
videoRef.current = null;
};
}, []);
return <div>Safe Video Player</div>;
}
export default SafeVideoPlayer;
In this version, the video playback is paused, the source is cleared, and the reference is explicitly set to null during unmount. This cleanup ensures the video element and associated resources are released, preventing leaks and keeping the application’s memory usage stable over time.
4. Conclusion
Memory leaks in React often arise from uncleaned side effects such as active intervals, lingering event listeners, unresolved asynchronous calls, retained refs, or unstable callback references. To prevent these issues, ensure every effect includes proper cleanup within useEffect, cancel ongoing network requests, remove event listeners and timers when components unmount, and clear useRef values once they’re no longer needed. Using Chrome DevTools and React Developer Tools to monitor memory usage and detect retained objects provides a practical way to identify and fix leaks early, keeping your application efficient and stable.
This article explored how to fix memory leaks in React apps.

