DEV Community

Cover image for Avoid memory leak caused by http calls on React using axios cancellation and redux
Fateh Mohamed 🐒
Fateh Mohamed 🐒

Posted on

Avoid memory leak caused by http calls on React using axios cancellation and redux

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

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

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);

Enter fullscreen mode Exit fullscreen mode

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

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;
        }
      }
    };
  }

Enter fullscreen mode Exit fullscreen mode

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

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

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

Top comments (0)