Do you remember when Uncle Ben said, "With great power comes great responsibility"? It turns out that the same holds true for React development. As React gains more and more popularity for frontend development, there are some bottlenecks that may eventually lead to less performant apps. One such reason could be Memory Leaks in React.
Explore the article to understand the concept of Memory Leaks in React, why React apps, especially Single Page Applications (SPAs), are susceptible to such leaks, and practical solutions using cleanup functions and AbortController APIs to optimize server resources and prevent Memory Leaks in large-scale applications.
Memory leaks occur when a computer program, in our case a React application, unintentionally holds onto memory resources that are no longer needed. These resources can include variables, objects, or event listeners that should have been released and freed up for other operations. Over time, these accumulated memory leaks can lead to reduced performance, slower response times, and even crashes.
React by far is widely used for creating Single Page Applications (SPAs), these SPAs fetches the entire JavaScript bundle from the server on initial request and then handles the rendering of each pages on the client side in the web browsers.
Note
React apps with SPA configuration do not entirely refresh when the URL path is changed, it just replaces the HTML content by updating the DOM tree through its Reconciliation process.
So, we have to be mindful while subscribing to memory in our React components because React will eventually change the HTML content according to any given page but the associated memory subscriptions (which could be a DOM Event listener, a WebSocket subscription, or even a request to an API ) may still be running in the background even after the page is changed !!
To better understand, consider the following examples:
import { Route, Routes, Link } from "react-router-dom";
import Home from "./Home";
import About from "./About";
import "./styles.css";
export default function App() {
return (
<div className="App">
<Link to="/about">About Page</Link>
<br />
<Link to="/">Home Page</Link>
<br />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</div>
);
}
App.js
const Home = () => {
return <>Home page</>;
};
export default Home;
import { useEffect } from "react";
const About = () => {
useEffect(() => {
setInterval(() => {
console.log("Hello World !");
}, 200);
}, []);
return <>About page</>;
};
export default About;
NoteThere is an Event Listener attached to the <About /> element. Therefore, each time the <About /> element is mounted into the DOM, the useEffect will be called and a fresh copy of the Event Listener will be created.
🔴 When we visit the “/about” next time the previous event listener and the newly created one both will start their execution. This cycle will keep on repeating as many times you toggle between these two components.
⚠️ This might not be significant for a small app like above but can seriously damage the overall performance if not handled in large scale React apps.
💡 To Overcome this issue, all we need to do is to cancel the memory subscription during the component’s unmounting time. We can do this with the clean up function inside the useEffect.
So, the refactored <About /> component will look like below :
import { useEffect } from "react";
const About = () => {
useEffect(() => {
const interval = setInterval(() => {
console.log("Hello World !");
}, 200);
return () => {
clearInterval(interval);
};
}, []);
return <>About page</>;
};
export default About;
With this small change, we are able to unsubscribe the memory allocated to the Event Listener every time the <About /> unmounts, which tackles the Memory Leaks and Improves the Overall performance.
Let’s say in one of your React components you are making an HTTP request that fetches the data from the server and later on after some processing on it, we want to set it into the state variable for UI generation.
But there is a catch, what if the user’s internet connection is slow and decides to move to another page ! in that case the web requests is already made so browser will expect some response even though the page is changed by user.
consider the below example:
import { Link, Routes, Route } from "react-router-dom";
import About from "./About";
import Home from "./Home";
export default function App() {
return (
<>
<div className="App">
<Link to="/about">About</Link>
<br />
<Link to="/">Home</Link>
<br />
</div>
<Routes>
<Route path="/about" element={<About />} />
<Route path="/" element={<Home />} />
</Routes>
</>
);
}
App.js
const Home = () => {
return <>Home page</>;
};
export default Home;
Home.js
import { useEffect, useState } from "react";
import axios from "axios";
const About = () => {
const [data, setData] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const { data } = await axios.get(
"https://jsonplaceholder.typicode.com/users"
);
// some extensive calculations on the received data
setData(data);
} catch (err) {
console.log(err);
}
};
fetchData();
}, []);
return (
<>
{/* handle data mapping and UI generation */}
About Page
</>
);
};
export default About;
If this is not take care of, it has a potential to unnecessarily occupy the server resources which indeed affect the maintenance cost of the servers.
Luckily, JavaScript provides a way to cancel the HTTP request whenever we want. via AbortControllers APIs. It represents a controller object that allows you to abort one or more Web requests as and when needed.
Look at the refactored <About /> below:
import { useEffect, useState } from "react";
import axios from "axios";
const About = () => {
const [data, setData] = useState(null);
useEffect(() => {
const abortController = new AbortController();
const fetchData = async () => {
let signal = abortController.signal;
try {
const { data } = await axios.get(
"<https://jsonplaceholder.typicode.com/users>",
{
signal: signal
}
);
// some extensive calculations on the received data
setData(data);
} catch (err) {
console.log(err);
}
};
fetchData();
return () => {
abortController.abort();
};
}, []);
return (
<>
{/* handle data mapping and UI generation */}
About Page
</>
);
};
export default About;
We added a cleanup function in our useEffect, which is just doing a job to abort the HTTP requests along with that extensive calculation whenever the <About /> is unmounted.
Note
That means aborting the request like above will directly get you inside the catch block when unmounting, so handle the catch block properly.
💡 Thus, by using the AbortController API, we can optimize the server resources and prevent Memory Leaks when building the large scale apps.
In this article you found out:
Thanks!!
A: Memory Leaks in React occur when the application unintentionally retains unnecessary memory resources. React SPAs, fetching the entire JavaScript bundle on the initial request, may encounter memory leaks when memory subscriptions (like DOM event listeners or API requests) persist even after a page change. This happens due to the SPA's nature, where only HTML content is replaced without a full page refresh, leading to unnoticed memory subscriptions running in the background.
A: Memory Leaks can be mitigated by incorporating cleanup functions inside the useEffect hook, ensuring the removal of unwanted memory subscriptions during component unmounting.
A: The AbortController API plays a crucial role in canceling HTTP requests, preventing unnecessary server resource occupation and optimizing memory usage, thus mitigating Memory Leaks in large-scale React applications.
Also, read: Why Sentry is a must tool for complex ReactJS App in 2023
One-stop solution for next-gen tech.