Creating a custom React hook that deals with promises
Tags: react, javascript
Published: 2020-05-04 13:21:06

Using Promises in React with hooks or with classes isn't as easy as it might seem at first. Let's look at a simple example to illustrate the problem:
```ts
const [result, setResult] = useState<string | undefined>(undefined)
useEffect(() => {
   promiseReturningFunction(a).then(res => setResult(res))
}, [a])
```
This code might not do what you want it to do - what's the problem with this implementation?
### 1.
Let's suppose that `a` is `1` at first, the request is sent and takes 1000ms, but `a` is changed immediately to `2`, therefore another request is sent and that one could return before the first one. Therefore the first request which returns after the second one and will override the value that is associated with `2`. That would result in the result of the `a = 1` request being displayed although `a` currently is `2`.
```
a = 1   a = 2   setResult(2)  setResult(1)       result = 1, a = 2 ?!?
  |       \----------/             |
  \--------------------------------/
```
### 2.
There is also another error which you might experience when using a dev build of react: A state update on an unmounted component (It's also a problem in if you are using a prod build of react but it won't notify you). When the component is unmounted while a promise is still pending the `.then` will call `setResult` although the component is no longer mounted:
```
request:       |------| setResult
component: |------| unmounted
```

The solution is quite simple: We have to "cancel" the request when the effect is supposed to do it's cleanup. Ok how can we achieve that? `useRef` to store the promise - sadly not because promises cannot be cancelled. What about a `useRef` to store a boolean variable called `cancelled`? Better but that will only handle the second problem. A simple variable scoped to the effect function will do the trick:
```ts
const [result, setResult] = useState<string | undefined>(undefined)
useEffect(() => {
   let cancel = false;
   promiseReturningFunction(a).then(res => {
      if (cancel) return;
      setResult(res)
   })
   return () => {
      cancel = true;
   }
}, [a])
```
Okay but that seems like a lot of code to write every time you want to consume some async function, it might be a better idea to extract this logic into a custom hook - let's call it `useAsync`.

Let's think about the parameters that such a hook could have:

- `fn: () => Promise<T>` (the function to call)
- `deps: any[]` (the deps of useEffect)

```ts
const useAsync = <T>(fn: () => Promise<T>, deps: any[]) => {
   const [res, setRes] = useState<T | undefined>();
   useEffect(() => {
      let cancel = false;
      fn().then(res => {
         if (cancel) return;
         setRes(res)
      })
      return () => {
         cancel = true;
      }
   }, deps)
   return res;
} 
```

Usage

```ts
const result = useAsync(() => fn(a), [a])
```

But it seems like at least two things are missing: a loading state and error handling - let's add them:

```ts
const useAsync = <T>(fn: () => Promise<T>, deps: any[]) => {
   const [loading, setLoading] = useState<boolean>(false);
   const [error, setError] = useState<Error | undefined>();
   const [res, setRes] = useState<T | undefined>();
   useEffect(() => {
      setLoading(true);
      let cancel = false;
      fn().then(res => {
         if (cancel) return;
         setLoading(false);
         setRes(res)
      }, error => {
         if (cancel) return;
         setLoading(false);
         setError(error);
      })
      return () => {
         cancel = true;
      }
   }, deps)
   return {loading, error, res};
} 
```

The problem here is not just limited to hooks. Class components in React have the same problem, but it mostly is ignored. It example shows that hooks are great for generically describing behavior without a lot of copy-pasting.