Picking Apart Kent's useAsync Hook

One of the snippet's I picked up from Kent C. Dodds' EpicReact course was this super cool useAsync function, and it's a masterclass in elegance. There are several patterns I want to call out that he uses here that every React dev should know.


1. Safe Dispatching: This is a technique to prevent memory leaks if the component unmounts. Fortunately, I've already written a short breakdown of this here
2. Returning a run function: Running an async function lends itself really well to abstracting more of the logic into the hook with this cool pattern
3. Returning setters: Again, this is a way to not make your fellow dev's mess around with your reducer actions


Without further ado, here's the code for you to skim. We'll break it down in a second.

function asyncReducer(state, action) {
  switch (action.type) {
    case 'pending': {
      return {status: 'pending', data: null, error: null}
    }
    case 'resolved': {
      return {status: 'resolved', data: action.data, error: null}
    }
    case 'rejected': {
      return {status: 'rejected', data: null, error: action.error}
    }
    default: {
      throw new Error('Unhandled action type')
    }
  }
}


function useAsync(initialState) {
  const [state, unsafeDispatch] = React.useReducer(asyncReducer, {
    status: 'idle',
    data: null,
    error: null,
    ...initialState,
  })

  const dispatch = useSafeDispatch(unsafeDispatch)

  const {data, error, status} = state

  const run = React.useCallback(
    promise => {
      dispatch({type: 'pending'})
      promise.then(
        data => {
          dispatch({type: 'resolved', data})
        },
        error => {
          dispatch({type: 'rejected', error})
        },
      )
    },
    [dispatch],
  )

  const setData = React.useCallback(
    data => dispatch({type: 'resolved', data}),
    [dispatch],
  )
  const setError = React.useCallback(
    error => dispatch({type: 'rejected', error}),
    [dispatch],
  )

  return {
    setData,
    setError,
    error,
    status,
    data,
    run,
  }
}
1. Using a safe dispatch: I'm not going to cover this right now because you can find a quick write up on it here. Just know that if you try to set state (dispatch) on an unmounted component, you'll have problems. I detail a simple hook guards against that.

2. Returning a run function

This one is great. All too often I've seen people return their state setting function from their hook without wondering whether it could be abstracted further.

function useAsync(initialState) {
  const [state, dispatch] = React.useReducer(asyncReducer)

  //...do some stuff

  return {
    state,
    dispatch
  }
}

Look familiar? Well it turns out we can do better. Much better.

Let's take a look at the code someone using this hook would have to write:

const [state, dispatch] = useAsync()

React.useEffect(() => {
  dispatch({type: 'pending'})
  fetch("https://api.example.com/items").then(
      data => {
        dispatch({type: 'resolved', data})
      },
      error => {
        dispatch({type: 'rejected', error})
      },
    )
},[])

Something like this. They have to run the fetch themselves, and resolve the promise with a dispatch. Is this the end of the world?

Of course not.

But why are we bothering them with our action types? We know everything that has to happen when that fetch is called.. Why not do it for them?

Enter Kent's run function:

const run = React.useCallback(
    promise => {
      dispatch({type: 'pending'})
      promise.then(
        data => {
          dispatch({type: 'resolved', data})
        },
        error => {
          dispatch({type: 'rejected', error})
        },
      )
    },
    [dispatch],
  )

We accept a promise, and handle things accordingly when it resolves. All of the chaining and worrying about action types has moved from our fellow dev's component into our hook. Neat!

This is how their implementation looks now:

const {state, run} = useAsync()

React.useEffect(() => {
  run(fetch("https://api.example.com/items"))
}, [])

Beautiful! But what if they need to set state manually? This brings me to our third pattern:


3. Returning Setters:

The run function makes it so we actually NEED to use this pattern, but it's a great one to keep in mind even when you don't need it because it is more widely applicable than the run function we just went over.

We aren't providing our fellow devs a dispatch anymore, so they have no way to force state. We need to provide one, so we'll just make some setters.

const setData = React.useCallback(
  data => dispatch({type: 'resolved', data}),
  [dispatch],
)
const setError = React.useCallback(
  error => dispatch({type: 'rejected', error}),
  [dispatch],
)

Hopefully you learned something here to help you write hooks a little bit more elegantly :)

I know I glossed over creating the reducer and some pieces here. Really I just wanted to call out these few patterns but if you have questions, feel free to email me at reid@43webstudio.com.