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
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.