
Preventing and Debugging Memory Leaks in React

Memory leaks in React are not always obvious, but they can cause performance problems that get worse over time. A memory leak happens when a component retains references to objects that should no longer exist, preventing them from being garbage collected. This leads to increased memory usage, making applications feel slower or less responsive.
The most frustrating part is that memory leaks often go unnoticed until an application has been running for a while. Fortunately, we can prevent them by following best practices, and when they do occur, we have tools to debug and fix them. In this article, I explore common causes of memory leaks, how to avoid them, and ways to track them down in a React application.
What Causes Memory Leaks in React?
React's automatic garbage collection usually handles memory cleanup, but certain patterns can cause memory to persist longer than necessary.
Common Causes of Memory Leaks
Unsubscribed Event Listeners
– If an event listener is added but not removed, React holds onto it indefinitely.Uncleared Timers and Intervals
– Functions likesetTimeoutandsetIntervalcontinue running unless they are stopped manually.Dangling API Subscriptions
– WebSockets, polling, or other API calls may keep a reference to a component after it unmounts.Updating State on an Unmounted Component
– If a component updates state after unmounting, React cannot properly clean it up.Holding Large Objects in State
– Keeping unnecessary data in memory can increase memory consumption unnecessarily.
So, let's take a look at how we can prevent these issues in our React applications.
Preventing Memory Leaks in React
We can avoid memory leaks by ensuring that React cleans up resources when a component unmounts. Here are the best ways to do that.
Cleaning up Event Listeners
Adding event listeners inside useEffect is common, but failing to remove them can lead to memory leaks.
Problem: Event Listener Remains After Unmounting
useEffect(() => { window.addEventListener("resize", () => console.log("Resized"));}, []);This event listener will remain active even after the component has been unmounted and gone.
Solution: Remove the event listener in useEffect cleanup
useEffect(() => { const handleResize = () => console.log("Resized"); window.addEventListener("resize", handleResize); return () => { window.removeEventListener("resize", handleResize); };}, []);The return portion of our useEffect is triggered as the component unmounts, allowing us to remove event listeners.
Clearing Timers and Intervals
Timers continue running unless explicitly cleared.
Problem: Timer Persists After Unmounting
useEffect(() => { setInterval(() => console.log("Still running"), 1000);}, []);Even if the component unmounts, this interval keeps running indefinitely.
Solution: Clear the Interval
You'll start to see a pattern with these solutions, I'm sure. The answer is to make sure that we clear the interval again in the hook's return. Like this:
useEffect(() => { const interval = setInterval(() => console.log("Still running"), 1000); return () => { clearInterval(interval); };}, []);Cancelling API Requests
If an API request resolves after a component unmounts, React may still try to update state on an unmounted component.
Problem: Fetch Request Continues After Unmounting
useEffect(() => { fetch("/api/data") .then((res) => res.json()) .then((data) => console.log(data));}, []);If the request is slow, it may still update state even after the component is removed.
Solution: Use AbortController to cancel pending requests
useEffect(() => { const controller = new AbortController(); fetch("/api/data", { signal: controller.signal }) .then((res) => res.json()) .then((data) => console.log(data)) .catch((err) => { if (err.name !== "AbortError") { console.error(err); } }); return () => { controller.abort(); };}, []);By aborting the controller, we can prevent unnecessary updates and ensure that the request is cancelled when the component unmounts.
Unsubscribing from Websockets and Event Streams
If a component subscribes to a WebSocket or external event stream, failing to unsubscribe means it remains in memory after unmounting.
Problem: Websocket Stays Open
useEffect(() => { const socket = new WebSocket("wss://example.com"); socket.onmessage = (event) => console.log(event.data);}, []);The WebSocket remains open, keeping the component alive in memory.
Solution: Close the Connection on Unmount
useEffect(() => { const socket = new WebSocket("wss://example.com"); socket.onmessage = (event) => console.log(event.data); return () => { socket.close(); };}, []);Debugging Memory Leaks in React
If memory usage keeps increasing even after components unmount, it's very likely that you have a memory leak. Here's some rough guidance on how to track it down.
Using Chrome Devtools
- Open DevTools (
F12orCmd + Option + I). - Go to the Performance tab and record while interacting with the app.
- Look at the JS Heap Size graph. If memory usage keeps growing without dropping, there may be a leak.
Tracking Detached Elements in Devtools
- Open the Memory tab in Chrome DevTools.
- Take a snapshot before and after navigating away from a component.
- If elements from the unmounted component are still present, they are not being garbage collected.
Preventing State Updates on Unmounted Components
If a component updates state after unmounting, we can use useRef to prevent updates.
const isMounted = useRef(true);useEffect(() => { return () => { isMounted.current = false; };}, []);const fetchData = async () => { const res = await fetch("/api/data"); const data = await res.json(); if (isMounted.current) { console.log(data); }};This ensures that state updates only occur if the component is still mounted.
Wrapping up
Memory leaks can cause slowdowns and unexpected behaviour in React applications, but they are avoidable. By properly cleaning up event listeners, timers, and API requests, we can prevent unnecessary memory retention. Debugging tools like Chrome DevTools help us detect leaks early, making it easier to keep applications running efficiently.
Key Takeaways
Memory leaks happen when components hold onto resources after unmounting.
Unsubscribed event listeners, timers, and API requests
are common causes of leaks.Cleaning up resources in
useEffectprevents unnecessary memory retention.Chrome DevTools and memory snapshots
help detect leaks in running applications.Using techniques like
AbortControlleranduseRefprevents state updates on unmounted components.
By keeping memory usage under control, we can make our React applications faster, smoother, and more reliable over time.
Related Articles

Creating Custom Vue Directives for Enhanced Functionality. 
Caching Strategies in React. Caching Strategies in React

How JavaScript Handles Memory Management and Garbage Collection. How JavaScript Handles Memory Management and Garbage Collection

Tips for Managing Memory in JavaScript. Tips for Managing Memory in JavaScript

Understanding the Difference Between <b> and <strong>. Understanding the Difference Between
<b>and<strong>
Function Declarations vs. Function Expressions vs. Arrow Functions. Function Declarations vs. Function Expressions vs. Arrow Functions

Use Greater‑Than and Less‑Than Symbols in JSX. Use Greater‑Than and Less‑Than Symbols in JSX

Life as a Freelance Developer in Brighton. Life as a Freelance Developer in Brighton

Building Design Systems for Web Applications with Figma, Storybook, and npm. Building Design Systems for Web Applications with Figma, Storybook, and npm
What is an HTML Entity? What is an HTML Entity?

Understanding CSS Positioning. Understanding CSS Positioning

Detecting Breakpoints in React Using Chakra UI. Detecting Breakpoints in React Using Chakra UI