Leo's dev blog

A custom hooks to use an async effect

Published on
2 mins read

Custom hooks to use an async effect

function useAsync(asyncCallback) {
  let [state, dispatch] = React.useReducer(asyncReducer)

  React.useEffect(() => {
    let promise = asyncCallback()
    if (!promise) return

    dispatch({ type: 'pending' })
    promise
      .then((data) => dispatch({ type: 'resolved', data }))
      .catch((error) => dispatch({ type: 'rejected', error }))
  }, [asyncCallback])

  return state
}

Usage:

function Component({ input }) {
  // Remember to wrap the async job in a useCallback
  let asyncCallback = React.useCallback(() => {
    if (!input) return

    // Run the async effect (fetch is an example)
    return fetch(input)
  }, [input])

  let { status, data, error } = useAsync(asyncCallback)

  switch (status) {
    case 'idle':
      return 'Waiting for the async to trigger'
    case 'pending':
      return 'Pending UI'
    case 'rejected':
      throw error
    case 'resolved':
      return 'Data UI'
    default:
      throw new Error('This should be impossible')
  }
}

How to clean the side effect (the async job start but then the component unmounted) ? - useSafeDispatch !

function useSafeDispatch(dispatch) {
  let mountedRef = React.useRef(false)
  React.useEffect(() => {
    mountedRef.current = true
    return () => {
      mountedRef.current = false
    }
  }, [])

  return React.useCallback(
    (...args) => {
      if (mountedRef.current) {
        dispatch(...args)
      }
    },
    [dispatch]
  )
}

Now change the useAsync function:

function useAsync(asyncCallback) {
  let [state, unsafeDispatch] = React.useReducer(asyncReducer)
  let dispatch = useSafeDispatch(unsafeDispatch)

  React.useEffect(() => {
    let promise = asyncCallback()
    if (!promise) return

    dispatch({ type: 'pending' })
    promise
      .then((data) => dispatch({ type: 'resolved', data }))
      .catch((error) => dispatch({ type: 'rejected', error }))
  }, [asyncCallback])

  return state
}

Cheers