No pun intended there!
The `useAsync()` hook which is developed by Kent C Dodds for his Epic React Workshop looks like this:
1function useSafeDispatch(dispatch) {
2 const mounted = React.useRef(false)
3 React.useLayoutEffect(() => {
4 mounted.current = true
5 return () => (mounted.current = false)
6 }, [])
7 return React.useCallback(
8 (...args) => (mounted.current ? dispatch(...args) : void 0),
9 [dispatch],
10 )
11}
12
13
14const defaultInitialState = {status: 'idle', data: null, error: null}
15function useAsync(initialState) {
16 const initialStateRef = React.useRef({
17 ...defaultInitialState,
18 ...initialState,
19 })
20 const [{status, data, error}, setState] = React.useReducer(
21 (s, a) => ({...s, ...a}),
22 initialStateRef.current,
23 )
24 const safeSetState = useSafeDispatch(setState)
25 const setData = React.useCallback(
26 data => safeSetState({data, status: 'resolved'}),
27 [safeSetState],
28 )
29 const setError = React.useCallback(
30 error => safeSetState({error, status: 'rejected'}),
31 [safeSetState],
32 )
33 const reset = React.useCallback(
34 () => safeSetState(initialStateRef.current),
35 [safeSetState],
36 )
37 const run = React.useCallback(
38 promise => {
39 if (!promise || !promise.then) {
40 throw new Error(
41 `The argument passed to useAsync().run must be a promise. Maybe a function that's passed isn't returning anything?`,
42 )
43 }
44 safeSetState({status: 'pending'})
45 return promise.then(
46 data => {
47 setData(data)
48 return data
49 },
50 error => {
51 setError(error)
52 return Promise.reject(error)
53 },
54 )
55 },
56 [safeSetState, setData, setError],
57 )
58
59
60 return {
61 isIdle: status === 'idle',
62 isLoading: status === 'pending',
63 isError: status === 'rejected',
64 isSuccess: status === 'resolved',
65
66
67 setData,
68 setError,
69 error,
70 status,
71 data,
72 run,
73 reset,
74 }
75}
76
77
78export {useAsync}
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.💣
1import * as React from 'react'
2import {
3 fetchBook,
4 BookInfoFallback,
5 BookForm,
6 BookDataView,
7 ErrorFallback,
8} from '../book'
9
10
11function BookInfo({bookName}) {
12 const [status, setStatus] = React.useState('idle')
13 const [book, setBook] = React.useState(null)
14 const [error, setError] = React.useState(null)
15
16
17 React.useEffect(() => {
18 if (!bookName) {
19 return
20 }
21 setStatus('pending')
22 fetchBook(bookName).then(
23 book => {
24 setBook(book)
25 setStatus('resolved')
26 },
27 error => {
28 setError(error)
29 setStatus('rejected')
30 },
31 )
32 }, [bookName])
33
34
35 if (status === 'idle') {
36 return 'Submit a book'
37 } else if (status === 'pending') {
38 return <BookInfoFallback name={bookName} />
39 } else if (status === 'rejected') {
40 return <ErrorFallback error={error}/>
41 } else if (status === 'resolved') {
42 return <BookDataView book={book} />
43 }
44
45
46 throw new Error('This should be impossible')
47}
48
49
50function App() {
51 const [bookName, setBookName] = React.useState('')
52
53
54 function handleSubmit(newBookName) {
55 setBookName(newBookName)
56 }
57
58
59 return (
60 <div className="book-info-app">
61 <BookForm bookName={bookName} onSubmit={handleSubmit} />
62 <hr />
63 <div className="book-info">
64 <BookInfo bookName={bookName} />
65 </div>
66 </div>
67 )
68}
69
70
71export default App
I am suuppperrr excited, let's do this!
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 is our code 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 the "Talk is cheap, show me the code" moment.
1import * as React from 'react'
2import {
3 fetchBook,
4 BookInfoFallback,
5 BookForm,
6 BookDataView,
7 ErrorFallback,
8} from '../book'
9import useAsync from '../utils';
10
11
12function BookInfo({bookName}) {
13 /////////////// Focus from here /////////////////
14 const {data: book, isIdle, isLoading, isError, error, run} = useAsync()
15
16
17 React.useEffect(() => {
18 if (!pokemonName) {
19 return
20 }
21 run(fetchPokemon(pokemonName))
22 }, [pokemonName, run])
23
24
25 if (isIdle) {
26 return 'Submit a book'
27 } else if (isLoading) {
28 return <BookInfoFallback name={bookName} />
29 } else if (isError) {
30 return <ErrorFallback error={error}/>
31 } else if (isSuccess) {
32 return <BookDataView book={book} />
33 }
34 //////////////// To here /////////////////
35
36
37 throw new Error('This should be impossible')
38}
39
40
41function App() {
42 const [bookName, setBookName] = React.useState('')
43
44
45 function handleSubmit(newBookName) {
46 setBookName(newBookName)
47 }
48
49
50 return (
51 <div className="book-info-app">
52 <BookForm bookName={bookName} onSubmit={handleSubmit} />
53 <hr />
54 <div className="book-info">
55 <BookInfo bookName={bookName} />
56 </div>
57 </div>
58 )
59}
60
61
62export default App
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 Epic React course.
I am also planning to share my learnings through such blogs in Future, Let's keep in touch!
Loves to Code 👨💻 | React Junkie ⚛️ | Learning and sharing 📚
No posts right now.