Asynchronous Flows...with React Hooks!
Tags: javascript, react
Published: 2019-09-06 18:45:30

I got the opportunity to implement some asynchronous data flows the other day at work, and I'd love to share my approach with y'all.


## Thought Process

Whenever I work with loading and displaying asynchronous data, I prefer separating the data loading work and the data displaying work into two components. For me, this separation of concerns helps me to focus on what a clean, easy to follow logic tree.

## Setting up our Loader

Here's what we want our loading component to handle:

- When the component mounts, we should trigger our api call to get our data.
- When this api call triggers, we should set some sort of loading state.
- When the api call is finished, we should set our data as state and indicate our loading has completed.
- We should pass this data down to some other component.

Based on that list, we need two pieces of state -- loading and data. We'll also need to figure out how to hook into our component's mounting. Let's start by setting up our state with the [useState hook](https://reactjs.org/docs/hooks-state.html).

```jsx
  import React, { useState } from 'React'
  import Breakfast from './Breakfast' // I utilize breakfast foods as my foo/bar/biz/baz

  const DataLoader = () => {
    const [ isLoading, setIsLoading ] = useState(false)
    const [ data, setData ] = useState([])
    
    return isLoading ? <div>Loading</div> : <Breakfast data={data} />
  }
```

Alright, state is set up! Now we need to make our API call. I'll split this into a new section to make things a little easier to follow.

## useEffect

 [useEffect](https://reactjs.org/docs/hooks-effect.html) is how we handle for mounts and updates. This function lets us capture side effects in our function components for use. The tl;dr of the documentation can be found here:

```javascript
  useEffect(callback, dependencyArray)
```

useEffect can be triggered in two ways: whenever a component mounts, and whenever the value of something in the dependencyArray changes. If you pass an empty array as the second argument, it will ensure useEffect only runs when your component mounts.

We'll be using an asynchronous function within useEffect. Of note - we cannot make our callback function asynchronous, because useEffect must either return a cleanup function or nothing. You'll see in a moment I use the [async/await](https://javascript.info/async-await) approach for Promise declaration. Implicitly, an async function returns a Promise, so without there being a point in time you could resolve what is now a promise-ified useEffect, it'll all blow up! But, using an async function within useEffect is totally fine.

```diff
-  import React, { useState } from 'React'
+  import React, { useState, useEffect } from 'React'
   import Breakfast from './Breakfast'

  const DataLoader = () => {
    const [ isLoading, setIsLoading ] = useState(false)
    const [ data, setData ] = useState([])
    
+   useEffect(() => {
+     async function fetchData() {
+       setIsLoading(true)
+       const fetcher = await window.fetch(/some/endpoint)
+       const response = await fetcher.json()
+       setData(response)
+       setIsLoading(false)     
+     }
+     fetchData()
    }, [])

    
    return isLoading ? <div>Loading</div> : <Breakfast data={data} />
  }
  
```

Here's how the above function works:
- With an empty dependency array, this useEffect will only run on mount.
- When the component mounts, run fetchData.
- Trigger our loading state. Utilize the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) (We made it happen!!!) to resolve a promise that gets us a response. 
- Resolve that promise using the `.json` function to parse the response.
- Set our data state to this response, and set our loading state to false.

At each point of the state changes, we'll have a re-render with the appropriate UI.

That's it for our loader! The component receiving our data is pretty standard as far as React components go, so I won't worry about that part of the example.

## Improvements

### Error Handling

There's some more we can do with our useEffect setup. Let's talk about error handling first.

`Async/Await` lends itself well to [try/catch/finally](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch) blocks, so let's give that a go. Let's extract the inner part of our useEffect and add try/catch/finally to it.

```diff
     async function fetchData() {
       setIsLoading(true)
+      try {
        const fetcher = await window.fetch(/some/endpoint)
        const response = await fetcher.json()
        setData(response)
+      } catch (error) {
+        // Do something with error
+      } finally {
+        setIsLoading(false)   
+      }  
     }
     fetchData()
  
```

The `try` portion will attempt to make our API call. If any error occurs, we will fall into our catch statement. After both of these complete, regardless of the result, we hit our finally block and clear out our loading state.

### Cleanup

It's a good idea to handle for a case where the component unmounts so that we don't continue setting state. `useEffect` supports cleanup functions which run when a component unmounts. Let's add that functionality in.

```diff
    useEffect(() => {
+    let didCancel = false
     async function fetchData() {
+      !didCancel && setIsLoading(true)
       try {
        const fetcher = await window.fetch(/some/endpoint)
        const response = await fetcher.json()
+       !didCancel && setData(response)
       } catch (error) {
           // Do something with error
       } finally {
+       !didCancel && setIsLoading(false)   
       }  
     }
     fetchData()
+    return () => { didCancel = true }
    }, [])
```

The returned function we added will run when the component unmounts. This will set didCancel to true, and ensure that all state is only set if `didCancel` is false.

## Final Words

There's a lot to unpack in this article. However, I wanted to get this out of my head an on to paper. I know other folks have written more in-depth pieces on this topic, but hopefully this encapsulates the challenging parts of leveraging useEffect with async. Please feel free to leave a comment below with any qs!