Cancel your promises when a component unmounts
Tags: react, hooks, promise, javascript
Published: 2020-03-21 08:25:23

In basically all React applications you will need to perform some async operations in your components. A common example would be to fetch the authenticated user on mount:

```js
import useDidMount from '@rooks/use-did-mount';
import { useState } from 'react';

export default () => {
  const [user, setUser] = useState();
  
  // Fetch the authenticated user on mount
  useDidMount(() => {
    fetchAuthenticatedUser().then((user) => {
      setUser(user);
    });
  });
  
  // Rest of the component...
};

```

At first glance, this all seems fairly valid, but it can cause the following 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.

So what does this mean? It is quite logical what happened if this error occurs in the above example, in that case:

- The component **unmounted before** the "fetchAuthenticatedUser" promise was **resolved**.

What means that if the promise *does* resolve:

- The **setUser** function is **called** on an **unmounted** component.

This is not allowed and to resolve this issue:

- The promise has to be **canceled** when the component **unmounts**.

So how are we going to fix this?

# Component still mounted?
First we need a way to check if a component is still mounted. We can do so
by making use of the **cleanup** function in a **useEffect** hook. 

> Every effect may return a function that cleans up after it.

So with the help of this cleanup function we can keep track of the mounted state and we can fix the potential error in the example code:

```js
import useDidMount from '@rooks/use-did-mount';
import { useState } from 'react';

export default () => {
  // Keep track of the mounted state
  const mountedRef = useRef<boolean>(false);
  const [user, setUser] = useState();
  
  // Fetch the authenticated user on mount
  useDidMount(() => {
    // Is mounted now
    mountedRef.current = true;

    fetchAuthenticatedUser().then((user) => {
      // Before calling "setUser", check if the component is still mounted
      if (mountedRef.current) {
        setUser(user);
      }
    });

    // Also in "useDidMount" we can use this cleanup function because it is just a wrapper around "useEffect"
    return () => {
      // Called before unmount by React
      mountedRef.current = false;
    };
  });
  
  // Rest of the component...
};
```
 
This will fix the potential error already. However, we probably need to do this in many components and therefore we can make it a little bit cleaner and more **DRY** with a custom hook called **useMountedState**:

# useMountedState
We basically want to extract the "mountedRef" part from above code in a custom hook. So we can then return a function which returns the current mounted state of the component:

```js
import { useCallback, useEffect, useRef } from 'react';

export default (): () => boolean => {
  const mountedRef = useRef<boolean>(false);

  // Basically the same as "useDidMount" because it has no dependencies
  useEffect(() => {
    mountedRef.current = true;

    return () => {
      // The cleanup function of useEffect is called by React on unmount
      mountedRef.current = false;
    };
  }, []);

  return useCallback(() => mountedRef.current, []);
};
```

Next, we can use this custom hook to make the fix a little bit cleaner:

```js
import useDidMount from '@rooks/use-did-mount';
import { useState } from 'react';
import useMountedState from './useMountedState';

export default () => {
  const isMounted = useMountedState();
  const [user, setUser] = useState();
  
  // Fetch the authenticated user on mount
  useDidMount(() => {
    fetchAuthenticatedUser().then((user) => {
      // Before calling "setUser", check if the component is still mounted
      if (isMounted()) {
        setUser(user);
      }
    });
  });
  
  // Rest of the component...
};
```

Already a little better, right? But we can do it even better with another custom hook, which will use the **useMountedState** hook internally. We will call this one **useCancelablePromise**:

# useCancelablePromise
The purpose of this hook is to create a wrapper function that we can use in our components around promises. So the hook needs to give us:

- A function which **accepts a promise** and **returns a promise**
- Where the returned promise resolves or rejects with the **result of the accepted/wrapped promise**
- Only when the component is still **mounted**

May sound a bit tricky, but it's pretty simple:

```js
import { useCallback } from 'react';
import useMountedState from './useMountedState';

export default () => {
  // Use our just created custom hook to keep track of the mounted state
  const isMounted = useMountedState();

  // Create our function that accepts a promise
  // Note the second parameter is a callback for onCancel. You might need this in rare cases
  return useCallback(<T>(promise: Promise<T>, onCancel?: () => void) =>
    // Wrap the given promise in a new promise
    new Promise<T>((resolve, reject) => {
      promise
        .then((result) => {
          // Only resolve the returned promise if mounted
          if (isMounted()) {
            // Resolve with the result of the wrapped promise
            resolve(result);
          }
        })
        .catch((error) => {
          // Only reject the returned promise if mounted
          if (isMounted()) {
            // Reject with the error of the wrapped promise
            reject(error);
          }
        })
        .finally(() => {
          // Call the onCancel callback if not mounted
          if (!isMounted() && onCancel) {
            onCancel();
          }
        });
    }),
  [isMounted]);
};
```

Now we can change our example code for the last time:

```js
import useDidMount from '@rooks/use-did-mount';
import { useState } from 'react';
import useCancelablePromise from './useCancelablePromise';

export default () => {
  const makeCancelable = useCancelablePromise();
  const [user, setUser] = useState();
  
  // Fetch the authenticated user on mount
  useDidMount(() => {
    makeCancelable(fetchAuthenticatedUser()).then((user) => {
      setUser(user);
    });
  });
  
  // Rest of the component...
};
```

Cleaner and still safe! Because the promise returned from **makeCancelable** is only resolved (or rejected) if the component is mounted :tada:

# Library
The source code of both custom hooks created in this article can be found on my Github:

[useMountedState](https://github.com/rodw1995/use-mounted-state)
[useCancelablePromise](https://github.com/rodw1995/use-cancelable-promise)

And they are also both published as a npm package. So you can use them directly in your React (native) project by just adding them:

```ssh
yarn add @rodw95/use-mounted-state
yarn add @rodw95/use-cancelable-promise
```

So make your promises cancelable and safe! Happy coding :grinning:

**Cheers**