A custom hooks to use an async effect

Published on
2 mins read
––– views

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