How to prevent React state update for asynchronous request on unmounted component?

I’m working on a mernstack app where I have a custom hook for API requests with useReducer’s state and dispatch functions that is loaded into the context api.

Usually GET request runs smoothly on page load, but every time I use the POST, PATCH, PUT, and DELETE request functions it causes a component to unmount and get this error:

Warning: 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.

The Main component displays the ParentList that is supplied with data from database on initial load through GET request inside useEffect simulating component did mount. I don’t know why it unmounts on event driven requests like post, patch, put and delete but the error goes away whenever I refresh the page and see the changes.

How to prevent React state update for asynchronous request on unmounted component?

Custom Hook for API Requests:

import { useEffect, useCallback, useReducer } from 'react';
import axios from 'axios';
import listReducer, { initialState } from '../../context/reducers/reducers';
import {
  loading,
  processingRequest,
  handlingError,
} from '../../context/reducers/actions/actionCreators';

const useApiReq = () => {
  const [state, dispatch] = useReducer(listReducer, initialState);

  const getRequest = useCallback(async () => {
    dispatch(loading());
    try {
      const response = await axios.get('/list');
      dispatch(processingRequest(response.data));
    } catch (err) {
      dispatch(handlingError);
    }
  }, []);

  const postRequest = useCallback(async (entry) => {
    dispatch(loading());
    try {
      const response = await axios.post('/list', entry);
      dispatch(processingRequest(response.data));
    } catch (err) {
      dispatch(handlingError);
    }
  }, []);

  const patchRequest = useCallback(async (id, updated_entry) => {
    dispatch(loading());
    try {
      const response = await axios.patch(`/list/${id}`, updated_entry);
      dispatch(processingRequest(response.data));
    } catch (err) {
      dispatch(handlingError);
    }
  }, []);

  const putRequest = useCallback(async (id, updated_entry) => {
    dispatch(loading());
    try {
      const response = await axios.put(`/list/${id}`, updated_entry);
      dispatch(processingRequest(response.data));
    } catch (err) {
      dispatch(handlingError);
    }
  }, []);

  const deleteRequest = useCallback(async (id) => {
    dispatch(loading());
    try {
      const response = await axios.delete(`/list/${id}`);
      dispatch(processingRequest(response.data));
    } catch (err) {
      dispatch(handlingError);
    }
  }, []);

  return [
    state,
    getRequest,
    postRequest,
    patchRequest,
    putRequest,
    deleteRequest,
  ];
};

export default useApiReq;

Context API:

import React, { createContext } from 'react';
import useApiReq from '../components/custom-hooks/useApiReq';

export const AppContext = createContext();

const AppContextProvider = (props) => {
  const [
    state,
    getRequest,
    postRequest,
    patchRequest,
    putRequest,
    deleteRequest,
  ] = useApiReq();

  return (
    <AppContext.Provider
      value={{
        state,
        getRequest,
        postRequest,
        patchRequest,
        putRequest,
        deleteRequest,
      }}
    >
      {props.children}
    </AppContext.Provider>
  );
};

export default AppContextProvider;

App:

import React from 'react';
import AppContextProvider from './context/AppContext';
import Header from './components/header/Header';
import Main from './components/main/Main';
import './stylesheets/styles.scss';

function App() {
  return (
    <AppContextProvider>
      <div className='App'>
        <Header />
        <Main />
      </div>
    </AppContextProvider>
  );
}

The error message is telling you the problem. You are using useEffect, but you aren’t checking whether the thing it’s effecting is mounted or not, so what happens is that, due to the timing of the async stuff, at some point you are trying to apply changes to something that isn’t mounted. This won’t do much (as the error says, this is a no-op), but not doing anything may not be what you want, and what you’re doing is definitely causing a memory leak. I assume you see it on the non-GET requests because they are taking longer, but it could be for some other reason – I can’t run your code as there is no example so I don’t know.

At the top of the useEffect set a variable like isMounted = true. Only fire the request if isMounted is true (ie wrap the request in an if statemt). In the function that gets returned from useEffect, set inMounted to false, ie

return () => {
  isMounted = false
}

Ideally, use a ref instead of just a variable, that’s what they’re there for - use createRef somewhere in the relevant component that is using the useEffect/s, then set a it to true, then check isMounted.current for the value.


Just for future reference, this is a bit too much to expect people to read through. 90% of that code is just your app, it doesn’t have anything to do with the issue. It would be helpful to have the actual thing causing the problem isolated in ideally one component & function and put up somewhere (you can mock async requests by using a timeout instead of fetch)

Also, could just use Redux instead of trying to hand-roll it – you’ve written the same thing, but you’re missing all the tooling + a lot of optimisations here that you get out of the box with react-redux, and for basically the same amount of code

At the very least, IMO don’t put the state and the function in the same context provider, have one for state (read) and one for the dispatch (write), then write a hook for dispatching and for reading.

1 Like

@DanCouper Thanks for your inputs, I intentionally decided to use Context API + useReducer instead of Redux as I want to test its capability. Just a follow up question, i read on stackoverflow to use the axios cancel token, and i also read here: https://www.robinwieruch.de/react-hooks-fetch-data where the author states: " Since Axios Cancellation has not the best API in my eyes, this boolean flag to prevent setting state does the job as well. - Your suggestion match with his approach. In your opinion which is the most ideal, using a variable or axios cancel token? Thanks again!

The token prevents the action on an unmounted component, but it doesn’t cancel the fetch request if it’s already been started. If it is important that a request is stopped, then you would use the cancellation API (or an AbortController if you were using fetch directly rather than Axios). If it doesn’t matter (normally doesn’t), then you can avoid the extra complexity and just use a token.

It tried Axios’s cancel token but i get the same warning, here’s my updated custom hook: