Avoid memory leak caused by http calls on React using axios cancellation and redux
Tags: react, axios, redux, performance
Published: 2023-03-01 09:25:23

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](https://rematchjs.org/) An easy redux framework. I recommend it 👍
- [Dummy delayed api](https://dummyapis.com/dummy/delay-response)

## Let's get started
If you prefer to read the code, i will not waste your time :) here is the code on [codesandbox](https://codesandbox.io/s/rematch-axios-cancellation-forked-ijupvu)

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]([Dummy delayed api](https://dummyapis.com/dummy/delay-response)) 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.

1. I avoid creating an AbortController instance in my component and pass it to my redux effect instead i do it effects object.

2. For each effect (http call) i set a global variable to an instance of AbortController

3. Before each (http call) i verify if there is a previous request executing by checking the AbortController instance (**slowAbortController in my code**).

4. 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.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rgz2ps5u8vqn3ypsxkvd.png)

**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 :)**


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gpf2q8s685ulumrvdbxm.png)

As a conclusion 

1. We make sure to have one running request at time.
2. Cancel running request if the user leave the current page and avoid memory leak.
3. We kept our component clean and declarative.

4. You can modify my code to cancel multiple request at the same time if needed

[Running code here](https://codesandbox.io/s/rematch-axios-cancellation-forked-ijupvu)