In a simple word, memory leak means subscriptions, data turning around that you don't need them. In React you have always to do cleaning when the component unmount.
Today we will focus on http request mainly to avoid unnecessary calls by aborting them while keeping our component clean (no aborting code inside components).
Let's consider these two scenarios:
- Multiple chained calls when you need to cancel the previous ones and keep the last one only
A good example for this scenario is the search feature, as the user types requests are triggered
- Slow http call triggered, but the user navigated away and leaves the component
This scenario means that your request is still running while the user doesn't need the response anymore
Tools
- axios (No big changes if you use fetch)
- Rematchjs An easy redux framework. I recommend it 👍
- Dummy delayed api
Let's get started
If you prefer to read the code, i will not waste your time :) here is the code on codesandbox
One of the common use cases is to load data when your component mounts same as the following code.
I am simulating a slow http call using this API with a delay of 6 seconds٫
🚀 We will make sure that the http call is cancelled if the user navigate to another page
Api.js
jsx
import React, { useEffect } from "react";
import { connect } from "react-redux";
import { useNavigate } from "react-router-dom";
import Layout from "./layout";
const App = ({ message, triggerSlowRequest, isLoading, cancelSlowRequest }) => {
const navigate = useNavigate();
const asyncCall = async () => await triggerSlowRequest();
useEffect(() => {
asyncCall();
return () => {
// redux action to cancel the http request on unmount
cancelSlowRequest();
};
}, []);
return (
<Layout>
<>
<div>
<button
onClick={() => {
navigate("/away");
}}
>
Navigate away (Component will be unmounted)
</button>
<br />
{/* A button to trigger multiple calls on multiple clicks*/}
<button onClick={asyncCall}>
New Call (Multiple calls scenario)
</button>
<div>
<h3>A delayed http call (6000ms)</h3>
Result: {isLoading ? "... LOADING" : message}
</div>
</div>
</>
</Layout>
);
};
const mapState = (state) => ({
message: state.message.message,
isLoading: state.loading.effects.message.asyncSlowRequest
});
const mapDispatch = (dispatch) => ({
cancelSlowRequest: dispatch.message.cancelSlowRequest,
triggerSlowRequest: dispatch.message.asyncSlowRequest
});
export default connect(mapState, mapDispatch)(App);
As you can see it's an ordinary React component that uses redux connect Since i'm using Rematch which is a great redux framework all my http calls are in my effects object (similar to redux-thunk).
All the ugly code goes there to make my components declarative the maximum.
I avoid creating an AbortController instance in my component and pass it to my redux effect instead i do it effects object.
For each effect (http call) i set a global variable to an instance of AbortController
Before each (http call) i verify if there is a previous request executing by checking the AbortController instance (slowAbortController in my code).
In case i find a previous one i abort it and proceed with the new one
rematch models.js
js
effects: (dispatch) => {
let slowAbortController;
return {
async asyncSlowRequest() {
if (slowAbortController?.signal) {
slowAbortController.abort();
dispatch.message.setError({
key: Date.now(),
message: "Previous call cancelled!"
});
}
slowAbortController = new AbortController();
const signal = {
signal: slowAbortController
? slowAbortController.signal
: AbortSignal.timeout(5000)
};
try {
const response = await axios.get(
"https://hub.dummyapis.com/delay?seconds=6",
signal
);
dispatch.message.setMessage(response.data);
slowAbortController = null;
} catch (error) {
console.log(error.name);
if (error.name === "CanceledError") {
console.error("The HTTP request was automatically cancelled.");
} else {
throw error;
}
}
},
async cancelSlowRequest() {
if (slowAbortController) {
slowAbortController.abort();
dispatch.message.setError({
key: Date.now(),
message: "Call cancelled after unmount"
});
slowAbortController = null;
}
}
};
}
I added a new effect cancelSlowRequest the one i use to clean my useEffect that can give me many possibilities as notifying the user if needed and on the other hand my component remains clean and declarative by keeping the cancellation control outside my component.
If you click 2 times on the second Btn (New Call) the first one will be cancelled
If you click on the first Btn (Navigate away) while the request is executing the request will be automatically cancelled and you are safe :)
As a conclusion
- We make sure to have one running request at time.
- Cancel running request if the user leave the current page and avoid memory leak.
We kept our component clean and declarative.
You can modify my code to cancel multiple request at the same time if needed