Simplifying Async State Management with the useAsync Hook

The useAsync hook simplifies the management of asynchronous operations in React by providing a declarative way to handle status

April 03, 2023

Managing the state of asynchronous operations in React can be a challenging task. We often find ourselves writing boilerplate code to handle loading, error, and success states. This can quickly become tedious and error-prone, especially when dealing with multiple async operations in a single component.

To simplify this process, we can create a custom hook called useAsync. This hook takes an async function as an input and returns the value, error, and status values we need to properly update our UI. Possible values for the status prop are: “idle”, “loading”, “success”, “error”. As you’ll see in the code below, our hook allows both immediate execution and delayed execution using the returned execute function.

Here’s an example of how you could implement the useAsync hook using TypeScript and the useReducer and useCallback hooks for better performance:

import { useReducer, useCallback, useEffect } from 'react' type State<T> = { data: T | unknown error: Error | null status: 'idle' | 'loading' | 'success' | 'error' } type Action<T> = | { type: 'reset' } | { type: 'loading' } | { type: 'success'; data: T } | { type: 'error'; error: Error } function asyncReducer<T>(state: State<T>, action: Action<T>): State<T> { switch (action.type) { case 'reset': return { data: null, error: null, status: 'idle', } case 'loading': return { ...state, status: 'loading', } case 'success': return { data: action.data, error: null, status: 'success', } case 'error': return { data: null, error: action.error, status: 'error', } default: throw new Error(`Unhandled action type`) } } export function useAsync<T>(asyncFunction: () => Promise<T>, immediate = true) { const [state, dispatch] = useReducer(asyncReducer, { data: null, error: null, status: 'idle', }) const execute = useCallback(() => { dispatch({ type: 'loading' }) asyncFunction() .then((data) => dispatch({ type: 'success', data })) .catch((error) => dispatch({ type: 'error', error })) }, [asyncFunction]) useEffect(() => { if (immediate) { execute() } }, [immediate]) return { ...state, execute } }

This hook allows you to handle the state of an asynchronous operation in a more declarative way. You can use it like this:

import { useAsync } from './useAsync' function MyComponent() { const fetchUser = async () => { const res = await fetch('https://jsonplaceholder.typicode.com/users') return res.json() } const { data, error, status, execute } = useAsync(fetchUser, false) // ... }

The useAsync hook can be used with any asynchronous function, not just for fetching data. Here’s an example of how we could use the useAsync hook to handle the state of a file upload operation:

import { useAsync } from './useAsync' function MyComponent() { const [file, setFile] = useState<File | null>(null) const { data, error, status, execute } = useAsync(() => uploadFile(file)) const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => { if (event.target.files) { setFile(event.target.files[0]) } } const handleUploadClick = () => { execute() } async function uploadFile(file: File | null) { if (!file) { throw new Error('No file selected') } const formData = new FormData() formData.append('file', file) const response = await fetch('/upload', { method: 'POST', body: formData, }) if (!response.ok) { throw new Error('Upload failed') } return response.json() } return ( <> <input type="file" onChange={handleFileChange} /> <button onClick={handleUploadClick}>Upload</button> {status === 'loading' && <p>Uploading...</p>} {status === 'success' && <p>Upload successful!</p>} {status === 'error' && <p>Upload failed: {error?.message}</p>} </> ) }

In this example, we’re using the useAsync hook to handle the state of a file upload operation. The uploadFile function is an async function that takes a File object as an input and uploads it to a server. We’re using the execute function returned by the useAsync hook to trigger the file upload when the user clicks the “Upload” button.

By using the useAsync hook, we can greatly simplify our async state management code and make it more readable and maintainable.


I hope this article helps you understand the useAsync hook and its benefits.