custom hooks

useAsync( )

The code provided in this article is simplified as much as possible to avoid overwhelming the reader with code and to convey the essence of the problem and its solution. In interactive examples, heading tags (`h1...h6`) are replaced with span tags to prevent potential conflicts with the main page structure.

Explanation

The useAsync custom hook simplifies handling asynchronous operations within React components. It provides an easy-to-use interface for executing asynchronous functions and managing their state, including the current status, result value, and any encountered errors. This abstraction streamlines the process of dealing with asynchronous code, making it more manageable and readable within React applications.

At its core, the hook exposes an execute function, which triggers the asynchronous operation. This function takes care of setting the appropriate status, clearing any previous values or errors, and then executing the provided asynchronous function with the given arguments. Upon completion, it updates the hook's state based on the success or failure of the operation.

Additionally, the hook offers an immediate parameter, which controls whether the provided asynchronous function should be executed immediately upon hook initialization. By default, immediate is set to true, so the asynchronous function runs right away. However, if immediate is explicitly set to false, the function will not execute until manually triggered through the execute function.

By encapsulating the complexity of asynchronous code handling, useAsync enhances code readability and maintainability, enabling developers to focus on building robust React components without getting bogged down in asynchronous intricacies.

Usage Cases

  • Asynchronous Data Handling: Employ the useAsync hook for managing asynchronous data fetching from APIs, ensuring efficient loading and error handling.
  • Form Submission: Simplify form submissions by utilizing the useAsync hook, enabling streamlined handling of server requests with loading feedback and error management.
  • Custom Asynchronous Tasks: Leverage the flexibility of the useAsync hook for handling various asynchronous operations within React components, enhancing code organization and readability.
  • Efficient Component Mounting: Optimize component mounting processes with the useAsync hook, executing asynchronous tasks only when necessary to improve application performance.
  • Dynamic UI Rendering: Dynamically update UI elements based on the status of asynchronous operations managed by the useAsync hook, providing users with real-time feedback.
  • Error Handling: Enhance error handling capabilities with the useAsync hook, ensuring graceful recovery from errors encountered during asynchronous operations for improved user experience.

Creation

Code implementation
1// Define and export an enum representing different states of the asynchronous operation
2export enum Status {
3 IDLE = 'idle',
4 PENDING = 'pending',
5 SUCCESS = 'success',
6 ERROR = 'error',
7}
8
9// Define an interface for the object returned from the `useAsync` hook
10interface AsyncState<T> {
11 status: Status;
12 value: T | null;
13 error: Error | null;
14 execute: (...args: unknown[]) => Promise<void>;
15}
16
17const useAsync = <T>(
18 asyncFunction: (...args: unknown[]) => Promise<T>,
19 immediate = true
20): AsyncState<T> => {
21 const [status, setStatus] = useState<Status>(Status.IDLE);
22
23 const [value, setValue] = useState<T | null>(null);
24
25 const [error, setError] = useState<Error | null>(null);
26
27 // The `execute` function wraps `asyncFunction` with the provided
28 // arguments and handles setting state;
29 // `useCallback` prevents `useEffect` from being called on every render,
30 // so the `useEffect` is triggered only when `asyncFunction` changes (its reference)
31 const execute = useCallback(
32 async (...args: unknown[]) => {
33 setStatus(Status.PENDING);
34
35 setValue(null);
36
37 setError(null);
38
39 try {
40 const response = await asyncFunction(...args);
41
42 setValue(response);
43
44 setStatus(Status.SUCCESS);
45 } catch (error) {
46 setError(error as Error);
47
48 setStatus(Status.ERROR);
49 }
50 },
51 [asyncFunction]
52 );
53
54 // Automatically run `execute` if `immediate` is `true` on component mount,
55 // alternatively, `execute` can be triggered manually (e.g., via a button click)
56 useEffect(() => {
57 if (immediate) {
58 execute();
59 }
60 }, [execute, immediate]); // Re-run `useEffect` only when any of these values change
61
62 return { execute, status, value, error };
63};
64
65export default useAsync;
66

Reference

Code implementation
1const { execute, status, value, error } = useAsync(asyncFunction, immediate?);
2

Parameters

NameTypeDescription
asyncFunction(...args: any[]) => Promise<T>An asynchronous function to be executed.
immediate (optional)booleanDetermines whether the async function should be executed immediately upon hook initialization. Defaults to `true`.

Return values

NameTypeDescription
execute(...args: any[]) => Promise<void>A function to trigger the asynchronous operation.
statusStatusThe current status of the asynchronous operation ("idle", "pending", "success", or "error").
valueT | nullThe result value of the asynchronous operation. Can be `null` if no value is available or if an error occurred.
errorError | nullThe `Error` object representing any encountered error during the asynchronous operation. `null` if no error occurred.

Example Usages

Fetch Users' Names upon button click

This example demonstrates how to use the useAsync hook to fetch data asynchronously from an API endpoint. The Container component utilizes the useAsync hook to handle the asynchronous operation, triggering data fetching upon button click. It dynamically renders different UI elements (Loader, Message, List) based on the current status of the asynchronous operation.

Code implementation
1// Function to fetch user data asynchronously
2const fetchUserNames = async () => {
3 const users = await loadData();
4
5 // Extract and return only users' names from fetched data
6 return users.map(({ id, name }) => ({
7 id,
8 name,
9 }));
10};
11
12const Container: FC = () => {
13 // Call the `useAsync` hook to handle the asynchronous operation,
14 // setting `immediate` to `false` to prevent function execution on component mount
15 const {
16 execute: handleFetch,
17 status,
18 value,
19 error,
20 } = useAsync(fetchUserNames, false);
21
22 return (
23 <Layer title="Container">
24 <div className="flex w-full flex-col items-center space-y-6">
25 {/* Button to trigger data fetching */}
26 <Button
27 onClick={handleFetch}
28 disabled={
29 status === Status.PENDING || status === Status.SUCCESS
30 }
31 >
32 {/* Display appropriate text based on the current `status` */}
33 {status === Status.PENDING
34 ? 'Loading...'
35 : status === Status.SUCCESS
36 ? 'Loaded'
37 : 'Load Users'}
38 </Button>
39 {/* Display a loader while fetching data */}
40 {status === Status.PENDING && <Loader />}
41 {/* Display an error message if fetching data failed */}
42 {status === Status.ERROR && error !== null && (
43 <Message type={MessageType.Error}>{error.message}</Message>
44 )}
45 {/* Display the list of users' names if data fetching succeeded */}
46 {status === Status.SUCCESS && value !== null && (
47 <List items={value} />
48 )}
49 </div>
50 </Layer>
51 );
52};
53
An interactive output of the code above

Form Data Submission

This example demonstrates the use of a custom useAsync hook to handle asynchronous form submission. The form collects user's name and email, and on submission, the button is disabled to prevent duplicate requests. The submission process displays a loader while the request is pending, followed by a success message if the server response is successful or an error message if the submission fails. The form fields are also disabled upon successful submission to ensure a seamless user experience. This setup emphasizes clear feedback to the user at every stage of the process.

Code implementation
1interface ServerResponse {
2 success: boolean;
3 message: string;
4}
5
6interface IFormData {
7 name: string;
8 email: string;
9}
10
11// Function to asynchronously submit form data to the server
12const submitFormData = async (...args: unknown[]): Promise<ServerResponse> => {
13 // Check if the first argument is a valid object representing form data
14 if (args.length > 0 && typeof args[0] === 'object' && args[0] !== null) {
15 const formData = args[0] as IFormData;
16
17 // Simulated request to the server (replace with a real request in production)
18 return new Promise((resolve, reject) => {
19 setTimeout(() => {
20 try {
21 resolve({
22 success: true,
23 message: 'Form submitted successfully!',
24 });
25 } catch (error) {
26 reject(error);
27 }
28 }, 2000); // Simulated server response delay (2 seconds)
29 });
30 } else {
31 // Handle cases where the provided argument is not valid form data
32 throw new Error('Invalid form data');
33 }
34};
35
36const Container: FC = () => {
37 const [formData, setFormData] = useState<IFormData>({
38 name: '',
39 email: '',
40 });
41
42 // Call the `useAsync` hook to handle the asynchronous operation,
43 // setting `immediate` to `false` to prevent function execution on mount
44 const { execute, status, value, error } = useAsync<ServerResponse>(
45 submitFormData,
46 false
47 );
48
49 const isEmpty = formData.name === '' || formData.email === '';
50
51 // Function to handle input changes
52 const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
53 const { name, value } = e.target;
54
55 setFormData((prevData) => ({
56 ...prevData,
57 [name]: value,
58 }));
59 };
60
61 // Function to handle form submission
62 const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
63 e.preventDefault();
64
65 // Check if both fields are filled
66 if (formData.name.trim() !== '' && formData.email.trim() !== '') {
67 // Execute request on form submission
68 execute(formData);
69 } else {
70 // If fields are empty, you can do something else, like showing an error
71 console.error('Name and Email are required');
72 }
73 };
74
75 return (
76 <Layer title="Container">
77 <form
78 className="flex w-full flex-col items-center justify-center space-y-4"
79 onSubmit={handleSubmit}
80 >
81 <div className="flex w-full flex-col gap-2">
82 <Label>Name</Label>
83 <Input
84 value={formData.name}
85 onChange={handleChange}
86 name="name"
87 disabled={status === Status.SUCCESS}
88 />
89 </div>
90 <div className="flex w-full flex-col gap-2">
91 <Label>Email</Label>
92 <Input
93 value={formData.email}
94 onChange={handleChange}
95 type="email"
96 name="email"
97 placeholder="example@rlu.dev"
98 disabled={status === Status.SUCCESS}
99 />
100 </div>
101 <Button
102 type="submit"
103 disabled={
104 status === Status.PENDING ||
105 status === Status.SUCCESS ||
106 isEmpty
107 }
108 >
109 {/* Display appropriate text based on the current `status` */}
110 {status === Status.PENDING
111 ? 'Sending...'
112 : status === Status.SUCCESS
113 ? 'Sent'
114 : 'Send'}
115 </Button>
116 </form>
117 <div className="flex w-full justify-center">
118 {/* Display a loader while submiting data */}
119 {status === Status.PENDING ? (
120 <Loader />
121 ) : (
122 <>
123 {/* Display an error message if the request fails */}
124 {status === Status.ERROR && error !== null && (
125 <Message type={MessageType.Error}>
126 {error.message}
127 </Message>
128 )}
129 {/* Display data if available */}
130 {status === Status.SUCCESS && (
131 <Message type={MessageType.Success}>
132 {value && value.success && value.message}
133 </Message>
134 )}
135 </>
136 )}
137 </div>
138 </Layer>
139 );
140};
141
An interactive output of the code above

Fetch Users' Emails upon component mount

This example utilizes the useAsync hook to perform an asynchronous operation upon component mount. By omitting the immediate parameter in the useAsync call, the request for users' emails is triggered immediately upon hook initialization, eliminating the need for additional useEffect usage. The UserEmailList component displays a list of users' emails with features to handle loading and error states.

Code implementation
1// Function to fetch user data asynchronously
2const fetchUserEmails = async () => {
3 const users = await loadData();
4
5 // Extract and return only users' emails from fetched data
6 return users.map(({ id, email }) => ({
7 id,
8 name: email,
9 }));
10};
11
12const UserEmailList: FC = () => {
13 // Call the `useAsync` hook without passing the `immediate` argument and without
14 // extracting `execute`, so the async function runs automatically on component mount
15 const { status, value, error } = useAsync(fetchUserEmails);
16
17 // Display a loader while data is being fetched
18 if (status === Status.PENDING) {
19 return <Loader />;
20 }
21
22 // Display an error message if the request fails
23 if (status === Status.ERROR && value !== null) {
24 return <Message type={MessageType.Error}>{error.message}</Message>;
25 }
26
27 // Display the list of users' emails if the data is loaded successfully
28 if (status === Status.SUCCESS && error !== null) {
29 return <List items={value} />;
30 }
31
32 return null;
33};
34
35const Container: FC = () => {
36 return (
37 <Layer title="Container">
38 <div className="flex w-full justify-center">
39 <UserEmailList />
40 </div>
41 </Layer>
42 );
43};
44
An interactive output of the code above
Last updated on by @skrykateSuggest an improvement on Github repository.