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}
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
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 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.
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
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!
Also don't forget to check other blogs of the series!
Top comments (7)
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 ๐คฃ
Lol ๐, I never saw it like that, might be an effect of NNN. ;)
Lmao same thing I thought.
What actually is the purpose of the useAsync hook? The name sounds vague, async what? When looking at the code all I'm seeing is lots of React abstractions coupled together. What problem are we solving with this hook?
Take a look at react-query. It is a superpower version of your useAsync hook.
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.
I think you should take a look at react-query.