DEV Community

Cover image for I promise this hook will blow your 1000+ lines of Async code
Harsh Choudhary
Harsh Choudhary

Posted on • Updated on • Originally published at harshkc.tech

I promise this hook will blow your 1000+ lines of Async code

No pun intended there! ;)

The useAsync() hook which I learned to build from Kent's Epic React Workshop looks like this:

function useSafeDispatch(dispatch) {
  const mounted = React.useRef(false)

  React.useLayoutEffect(() => {
    mounted.current = true
    return () => (mounted.current = false)
  }, [])
  return React.useCallback(
    (...args) => (mounted.current ? dispatch(...args) : void 0),
    [dispatch],
  )
}

const defaultInitialState = {status: 'idle', data: null, error: null}

function useAsync(initialState) {
  const initialStateRef = React.useRef({
    ...defaultInitialState,
    ...initialState,
  })
  const [{status, data, error}, setState] = React.useReducer(
    (s, a) => ({...s, ...a}),
    initialStateRef.current,
  )

  const safeSetState = useSafeDispatch(setState)

  const setData = React.useCallback(
    data => safeSetState({data, status: 'resolved'}),
    [safeSetState],
  )
  const setError = React.useCallback(
    error => safeSetState({error, status: 'rejected'}),
    [safeSetState],
  )
  const reset = React.useCallback(
    () => safeSetState(initialStateRef.current),
    [safeSetState],
  )

  const run = React.useCallback(
    promise => {
      if (!promise || !promise.then) {
        throw new Error(
          `The argument passed to useAsync().run must be a promise. Maybe a function that's passed isn't returning anything?`,
        )
      }
      safeSetState({status: 'pending'})
      return promise.then(
        data => {
          setData(data)
          return data
        },
        error => {
          setError(error)
          return Promise.reject(error)
        },
      )
    },
    [safeSetState, setData, setError],
  )

  return {
    isIdle: status === 'idle',
    isLoading: status === 'pending',
    isError: status === 'rejected',
    isSuccess: status === 'resolved',

    setData,
    setError,
    error,
    status,
    data,
    run,
    reset,
  }
}

export {useAsync}
Enter fullscreen mode Exit fullscreen mode

We will be using our hook to refactor the BookInfo component below and make it more elegant and robust by blowing multiple lines of code.💣

import * as React from 'react'
import {
  fetchBook,
  BookInfoFallback,
  BookForm,
  BookDataView,
  ErrorFallback,
} from '../book'

function BookInfo({bookName}) {
  const [status, setStatus] = React.useState('idle')
  const [book, setBook] = React.useState(null)
  const [error, setError] = React.useState(null)

  React.useEffect(() => {
    if (!bookName) {
      return
    }
    setStatus('pending')
    fetchBook(bookName).then(
      book => {
        setBook(book)
        setStatus('resolved')
      },
      error => {
        setError(error)
        setStatus('rejected')
      },
    )
  }, [bookName])

  if (status === 'idle') {
    return 'Submit a book'
  } else if (status === 'pending') {
    return <BookInfoFallback name={bookName} />
  } else if (status === 'rejected') {
    return <ErrorFallback error={error}/>
  } else if (status === 'resolved') {
    return <BookDataView book={book} />
  }

  throw new Error('This should be impossible')
}

function App() {
  const [bookName, setBookName] = React.useState('')

  function handleSubmit(newBookName) {
    setBookName(newBookName)
  }

  return (
    <div className="book-info-app">
      <BookForm bookName={bookName} onSubmit={handleSubmit} />
      <hr />
      <div className="book-info">
        <BookInfo bookName={bookName} />
      </div>
    </div>
  )
}

export default App

Enter fullscreen mode Exit fullscreen mode

I am suuppperrr excited, let's do this!

Super Excited GIF

But before we move ahead let's get on the same page.

  • fetchBook fetches data from the API and results in Promise which returns book data on resolution and error on rejection.

  • BookInfoFallback is your loader component that accepts bookName to display a nice loading effect.

  • BookForm is a simple form component that takes data from users.

  • BookDataView is a nice looking component that displays the Book data to the user.

  • ErrorFallback to show nice looking UI with Error.

Implementation of these components is beyond this blog but they are just regular stuff.

What the hell our code is doing?

It is taking the bookName from the user and passing that to the BookInfo component which handles fetching of the bookData in the useEffect hook which sets the state according to different conditions, it also handles the rendering of BookDataView upon successful fetching, ErrorFallback on failure, and BookInfoFallback while loading.

Ok I might have triggered

"Talk is cheap, show me the code" moment.

Confused GIF

import * as React from 'react'
import {
  fetchBook,
  BookInfoFallback,
  BookForm,
  BookDataView,
  ErrorFallback,
} from '../book'
import useAsync from '../utils';

function BookInfo({bookName}) {
  /////////////// Focus from here /////////////////
  const {data: book, isIdle, isLoading, isError, error, run} = useAsync()

  React.useEffect(() => {
     if (!pokemonName) {
       return
     }
     run(fetchPokemon(pokemonName))
  }, [pokemonName, run])

  if (isIdle) {
    return 'Submit a book'
  } else if (isLoading) {
    return <BookInfoFallback name={bookName} />
  } else if (isError) {
    return <ErrorFallback error={error}/>
  } else if (isSuccess) {
    return <BookDataView book={book} />
  }
 //////////////// To here /////////////////

  throw new Error('This should be impossible')
}

function App() {
  const [bookName, setBookName] = React.useState('')

  function handleSubmit(newBookName) {
    setBookName(newBookName)
  }

  return (
    <div className="book-info-app">
      <BookForm bookName={bookName} onSubmit={handleSubmit} />
      <hr />
      <div className="book-info">
        <BookInfo bookName={bookName} />
      </div>
    </div>
  )
}

export default App

Enter fullscreen mode Exit fullscreen mode

Woah isn't that neat now, not only does it make our code more readable, we have made our component more robust by not calling the dispatch when the component is unmounted, also we have memoized our fetch method to save network calls if the bookName doesn't change.

But but Harsh aren't we writing more code to accomplish pretty common stuff?

Yes, we are but by writing that hook we can refactor multiple components throughout the project using Async code like that, see in terms of cumulative time saved, less code shipped and high confidence gain.

This is the first part of the useAsync() hook which demonstrates its use cases.

In the next, we will decouple the hook and build it from scratch explaining each line and learning neat tricks.

We will also test the hook in part 3 because why not?

Are you excited about the real deal in part 2? Do tell in the comments and share this article with your friends and excite them too.

A little intro about me, I want to make the world a better place through innovative and quality software.
Does that sound familiar?
Yeah, I am a big Kent C. Dodds fan, he is an inspiration for many.

This hook is used extensively throughout his Epic React workshop. Go check out his awesome course here.

I am also planning to share my learnings through such blogs in Future, Let's keep in touch!

Portfolio Twitter Linkedin

Also don't forget to check other blogs of the series!

Discussion (6)

Collapse
inhuofficial profile image
InHuOfficial • Edited on

I promise this hook will blow your 1000+ lines of Async code

Think that title might be missing a word as at the moment it sounds like “hook” is going to make “1000+ lines of async code” very happy 🤣

Collapse
cjsmocjsmo profile image
Charlie J Smotherman

Lmao same thing I thought.

Collapse
harshkc profile image
Harsh Choudhary Author

Lol 😂, I never saw it like that, might be an effect of NNN. ;)

Collapse
ochrstn profile image
ochrstn

Take a look at react-query. It is a superpower version of your useAsync hook.

Collapse
harshkc profile image
Harsh Choudhary Author

Yeah basically it made so that you can easily migrate to react-query, but still I believe there are lot of cases when you don't want to use react query, this will help and maintain that consistency.

Collapse
ivanjeremic profile image
Ivan Jeremic

I think you should take a look at react-query.