1、
内存泄露发生的场景
call setState
on a component that's no longer mounted
https://www.loginradius.com/blog/engineering/how-to-fix-memory-leaks-in-react/
A memory leak is a commonly faced issue when developing React applications. It causes many problems, including:
- affecting the project's performance by reducing the amount of available memory;
- slowing down the application; and
- crashing the system.
So, you need to eliminate memory leaks issues.
You may encounter the following warning message in React application when working with asynchronous calls:
Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
React can't detect memory leaks directly, but it introduces a warning to guide you to help figure them out on your own.
Primary Causes Of Memory Leaks
React components that perform state updates and run asynchronous operations can cause memory leak issues if the state is updated after the component is unmounted. Here is a normal scenario that causes this memory leak issue:
- The user performs an action that triggers an event handler to fetch data from an API.
- After that, a user clicks on a link, which navigates to another page before completing step #1.
- Now, the first action completes and passes the data retrieved from the API and calls function, which updates the state.
Since the component was unmounted and function is being called in a component that is no longer mounted, it causes memory leak issue -- and in the console, you'll get a warning.
Example of unsafe code:
const [value, setValue] = useState('checking value...');
useEffect(() => {
fetchValue().then(() => {
setValue("done!"); // ⚠️ what if the component is no longer mounted ?
// we got console warning of memory leak
});
}, []);
Fixes for Memory Leaks
There are a few ways to eliminate memory leaks. Some of them are as follows.
1) Using Boolean Flag
const [value, setValue] = useState('checking value...');
useEffect(() => {
let isMounted = true;
fetchValue().then(() => {
if(isMounted ){
setValue("done!"); // no more error
}
});
return () => {
isMounted = false;
};
}, []);
In the above code, I've created a boolean variable isMounted
, whose initial value is true. When isMounted
is true, the state is updated and function is returned. Else if the action is unmounted before completion, then function is returned with isMounted
as false. This ensures that when a new effect is to be executed, the previous effect will be first taken care of.
2) Using AbortController
useEffect(() => {
let abortController = new AbortController();
// your async action is here
return () => {
abortController.abort();
}
}, []);
In the above code, I've used AbortController
to unsubscribe the effect. When the async action is completed, then I abort the controller and unsubscribe the effect.
3) Using use-state-if-mounted Hook
const [value, setValue] = useStateIfMounted('checking value...');
useEffect(() => {
fetchValue().then(() => {
setValue("done!"); // no more error
});
}, []);
In the above code, I've used a hook that works just like React's useState
, but it also checks that component is mounted before updating the state!
Conclusion
In this article, you've learned what React memory leaks are and how to use three different methods to fix memory leaks.
https://dev.to/jeremiahjacinth13/memory-leaks-how-to-avoid-them-in-a-react-app-1g5e
What is a memory leak?
According to Wikipedia, a memory leak is a type of resource leak that occurs when a computer program incorrectly manages memory allocations in a way that memory that is no longer needed is not released. A memory leak may also happen when an object is stored in memory but cannot be accessed by the running code.
Simply put, a memory leak is said to occur whenever inaccessible or unreferenced data exists in memory. Nowadays, many modern programming languages have techniques for clearing out data that is no longer needed, garbage collection, but it turns out there are other not-so-popular errors which can expose your React app to memory leaks and, to a great extent, reduce the performance of your app.
Let's look at some causes of memory leaks.
Causes of Memory Leaks in a React Application
Memory leaks in React applications are primarily a result of not cancelling subscriptions made when a component was mounted before the component gets unmounted. These subscriptions could be a DOM Event listener, a WebSocket subscription, or even a request to an API.
The first two are not too much of a challenge, as we can easily remove an event listener or unsubscribe from the WebSocket before the component gets unmounted. But the last one might require a little bit of extra work.
A typical React workflow
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import axios from 'axios';
const MyCompany = function() {
const [ company, setCompany ] = useState(null);
useEffect(() => {
(async () {
const { data } = await axios.get(
'https://random-data-api.com/api/company/random_company'
);
setCompany(data);
})();
}, []);
return (
<>
<pre>{JSON.stringify(company, null, 3)}</pre>
<Link to = '/anotherpage'>Another Interesting Page</Link>
</>
)
}
In the code snippet above, we have a simple component MyCompany
which when mounted, makes a request to get a random company and sets value of company
state to the value gotten from the API.
The Problem
Assuming our user has a very slow internet connection and then decides to leave the current page for another interesting page, the request would have already been made and our browser would be expecting a response, which when received, would lead us to call setState
on a component that's no longer mounted.
Aside from setting state, we would now have unimportant data in our app with no means of accessing them. This process is repeated multiple times while the user uses the app, filling up useful memory with useless and inaccessible data and leading to serious performance issues.
We've seen the problems and I believe you understand, now let's look at how to solve this problem.
The Way Forward: AbortControllers
Having understood the problem, what we'd do to salvage the situation is cancel the request the moment our component unmounts, ensuring we don't get any data from the API.
So, how do we cancel requests? AbortControllers
According to MDN, the AbortController represents a controller object that allows you to abort one or more Web requests as and when desired. That's quite explanatory!!
AbortControllers are created with the new AbortController()
syntax, initializing an instance of the AbortController class. Every AbortController object has a read-only signal
property which is passed into requests, and an abort()
method which is whenever you want to cancel a request.
Now using AbortControllers, our code should look like this:
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import axios from 'axios';
const MyCompany = function() {
const [ company, setCompany ] = useState(null);
useEffect(() => {
let abortController;
(async () {
abortController = new AbortController();
let signal = abortController.signal;
// the signal is passed into the request(s) we want to abort using this controller
const { data } = await axios.get(
'https://random-data-api.com/api/company/random_company',
{ signal: signal }
);
setCompany(data);
})();
return () => abortController.abort();
}, []);
return (
<>
<pre>{JSON.stringify(company, null, 3)}</pre>
<Link to = '/anotherpage'>Another Interesting Page</Link>
</>
)
}
Now, when our user navigates to a new page, our AbortController cancels the request and we don't have to worry about having data leaks.
NOTE: Calling abortController.abort() after the request has been completed doesn't throw any errors. The abortController simply does not take any action on an already complete request.
Using AbortControllers in your web apps can help improve performance and prevent memory leaks, so it's something you should actually use.
Thanks for reading❤❤