Memory leak React
Today we'll dive deeper into memory leaks in React, exploring the concepts, prevention techniques, and debugging process.
What Is a Memory Leak? An Analogy
Imagine that each component in your application is a tenant in a building (your program's memory). When a tenant (component) moves out (is unmounted), they must return the keys and cancel all the services they signed up for (subscriptions, timers, etc.).
A memory leak happens when the tenant moves out but forgets to cancel a service, like a magazine subscription. The magazine (the resource) continues to be delivered to an empty apartment, and the building's management (JavaScript) cannot rent that space to a new tenant because there is still a pending item associated with it. Over time, several apartments get stuck in this state, and the building runs out of useful space, making everything slower and more inefficient.
In React, the tenant is the component, and the services are asynchronous operations like API calls, setInterval, or addEventListener.
A Deeper Look at Causes and Solutions
1. Asynchronous Operations and Component State
The core of the problem lies in the interaction between the component's lifecycle and the asynchronous nature of JavaScript.
Advanced Solution: Custom Hook for Mount State
An elegant approach is to create a custom hook that tracks whether the component is still mounted. This simplifies the logic inside your components.
// hooks/useIsMounted.js
import { useRef, useEffect } from 'react';
export function useIsMounted() {
const isMountedRef = useRef(true);
useEffect(() => {
// The useEffect cleanup function is the perfect place to update the ref
return () => {
isMountedRef.current = false;
};
}, []); // The empty array ensures this runs only on mount and unmount
return isMountedRef;
}
// How to use it in your component:
import { useState, useEffect } from 'react';
import { useIsMounted } from './hooks/useIsMounted';
function MyComponent() {
const [data, setData] = useState(null);
const isMounted = useIsMounted(); // Our custom hook
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(result => {
// ONLY UPDATE STATE IF THE COMPONENT IS STILL MOUNTED
if (isMounted.current) {
setData(result);
}
});
}, []); // Dependencies for your fetch
}
This approach is more declarative and cleaner than AbortController for simple cases, although AbortController is more robust because it actually cancels the network request, saving bandwidth.
2. Event Handlers on Global Objects (window, document)
Adding events to objects that exist outside of React's lifecycle (like window) is a common source of leaks.
Example of what NOT to do:
useEffect(() => {
// WRONG: A new function is created on every render
window.addEventListener('scroll', () => console.log(window.scrollY));
return () => {
// THIS DOESN'T WORK! It's a different function instance.
window.removeEventListener('scroll', () => console.log(window.scrollY));
};
}, []);
Correct Example:
useEffect(() => {
// CORRECT: The function is defined once, and its reference is preserved.
const handleScroll = () => console.log(window.scrollY);
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll); // Removes the same function
};
}, []);
How to Debug and Find Memory Leaks in Practice
Using the Chrome DevTools is the most effective way to confirm a leak.
Detailed Step-by-Step Guide:
Another Useful Tool: console.log in the Cleanup Function
A simple but effective technique is to place a console.log in your useEffect cleanup function to ensure it's being called when the component unmounts.
useEffect(() => {
// ... your effect logic
return () => {
console.log('Component unmounted, cleanup executed!');
};
}, []);
Don't forget to delete this console.log before uploading the code to production
If you navigate away from the page and don't see this message in the console, the cleanup function is not being triggered as expected, which can be a clue.
In summary, the key to preventing memory leaks in React is discipline: for every resource you open, subscribe to, or schedule in a useEffect, you must have corresponding logic to close, unsubscribe, or clear it in the return (cleanup) function of that same useEffect.
Software Engineer | Python, Django, AWS, RAG
2wThanks for sharing, Fernando
FullStack Software Engineer | React.js | Next.js | Typescript | Zustand | Radix | Material UI | Tailwind CSS | SCSS | Node.js | Django
3wGreat post!
Back End Engineer | Software Engineer | TypeScript | NodeJS | ReactJS | AWS | MERN | GraphQL | Jenkins | Docker
3wThanks for sharing 🚀
Software Engineer | Mobile Developer | Flutter | Dart
3wThanks for sharing!
Software engineer | Frontend Developer | React.Js
4wGreat content!