Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?

How to suppress "Can't perform React state update on unmounted component" All of us would have experienced the following error when working with React. This error happens if we try to perform state update (call setState) after a component has been unmounted. React warns us that this causes memory leak with this message. This is just a warning but not a show stopper. But it is our responsibility to fix it to avoid memory leaks.

cannot-perform-state-update

When does this occur usually occur

You have a asynchronous request to a backend API, you click a button that triggers the API, but you were not patient enough to wait for the response and navigate to a different route. You have a event listener but fail to clean them up when the component unmounts You have some setTimeout or setInterval that gets called after a component has unmounted and it calls some setState operation. In any case, the side effects have to be cleaned up correctly when the component unmounts. This may not be a problem at the initial stages or if the webpage is shortlived. But over time, the memory leaks can accumulate and will slow down the application. In my previous company, our CEO wanted all the web apps to be kept open without refreshing or closing for a few days to see whether the app slows down because of memory leaks. In cases like this, if your application does not prevent memory leaks, this will significantly slow down the application.

In this blog, we will see how to prevent these kind of memory leaks when making asynchronous requests.

There are two types of solution to the problem.

*Corresponding gist:https://gist.github.com/aishwarya257/6b9b0e0c0e80e075c37e2a5a88b3232f *

Check if component unmounted before setting setState/dispatching useReducer's action export const FETCH_STATUS = { pending: "pending", resolved: "resolved", idle: "idle", rejected: "rejected" } as const;

export type FetchStatus = typeof FETCH_STATUS[keyof typeof FETCH_STATUS]; type ResolvedFetch = typeof FETCH_STATUS.resolved;

export type FetchAction = | { type: Exclude<FetchStatus, ResolvedFetch> } | { type: ResolvedFetch; data: T }

export type FetchState = | { status: Exclude<FetchStatus, ResolvedFetch> data: null; } | { status: ResolvedFetch; data: T; }

export function fetchReducer( state: FetchState, action: FetchAction ): FetchState { switch (action.type) { case FETCH_STATUS.pending: return { status: FETCH_STATUS.pending, data: null }; case FETCH_STATUS.resolved: return { status: FETCH_STATUS.resolved, data: action.data }; default: return state; } }

function useFetch<T = unknown>(initialData: Record<string, unknown> = {}) { const [state, unsafeDispatch] = useReducer< Reducer<FetchState, FetchAction>

(fetchReducer, { status: FETCH_STATUS.idle, data: null, ...initialData });

const dispatch = useSafeDispatch<FetchAction>(unsafeDispatch); const call = useCallback( (url) => { dispatch({ type: FETCH_STATUS.pending }); fetch(url).then( (response: Response) => response.json().then((data: T) => { dispatch({ type: FETCH_STATUS.resolved, data}); }), ); }, [dispatch] ); return [state, call] as const; }

export default useFetch; This is a simple reducer, that makes a asynchronous request to an API. But if you note line number 53, the dispatch function of useReducer calls the hook useSafeDispatch which takes dispatch function of the useReducer as an argument.

This hook in turn returns another dispatch function. Let's see what the hook does in the below code.

Corresponding gist: https://gist.github.com/aishwarya257/d69f540c065e2387ef32e1e29c294df6 * import { Dispatch, useCallback, useLayoutEffect, useRef } from "react";

export function useSafeDispatch(dispatch: Dispatch) { const mounted = useRef(false); useLayoutEffect(() => { mounted.current = true; return () => { mounted.current = false; }; }, []);

return useCallback((arg:T) => {
    if(mounted.current) {
        dispatch(arg)
    }
}, [dispatch]);

} The hook here returns a function, the return value is wrapped with useCallback to avoid unnecessary re-renders. In general, you don't have to specify dispatch function of useReducer in the dependency array as it is consistent during re-renders. But ESLint could not determine that since the value is passed over to another function.

The return function body checks if mounted.current is true, only then it calls the dispatch function which means setting the state.

But, the problem is this solution does not completely remove the memory leak, but it just suppresses the warning that React signals us.

The memory leak is not avoided because the asynchronous request is still made in this approach despite preventing state setting.

Let's look at another solution which combines the above approach with cancelling the actual API request

Corresponding gist: https://gist.github.com/aishwarya257/91b330217707bafe9742984f7661adcc * function useCancelableFetch<T = unknown>(initialData: Record<string, unknown> = {}) { const [state, unsafeDispatch] = useReducer< Reducer<FetchState, FetchAction> >(fetchReducer, { status: FETCH_STATUS.idle, data: null, ...initialData }); const controllerRef = useRef(new AbortController()); useEffect(() => { const controller = controllerRef.current; return () => { controller.abort(); } }, [])

const dispatch = useSafeDispatch<FetchAction<T>>(unsafeDispatch);
const call = useCallback(
  async (url) => {
    const controller = controllerRef.current;
    dispatch({ type: FETCH_STATUS.pending });
    try {
      const response = await fetch(url, {signal: controller.signal});
      const data = await response.json();
      dispatch({type: FETCH_STATUS.resolved, data})
    } catch(e) {
      console.log(e)
    }
  },re
  [dispatch]
);
return [state, call] as const;

}

export default useCancelableFetch; Javascript has something called AbortController which allows you cancel any asynchronous API request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment