Implementing a Reusable useAsync Hook in React with TypeScript
The useAsync hook is a common pattern in React applications for managing asynchronous operations like fetching data, performing calculations, or interacting with APIs. This challenge asks you to implement a generic useAsync hook that handles the lifecycle of an asynchronous function, providing state management for loading, error, and data, and allowing for manual triggering of the async function. This hook promotes code reusability and simplifies asynchronous logic within your components.
Problem Description
You need to implement a custom useAsync hook in React using TypeScript. This hook should accept an asynchronous function as an argument and manage its execution, providing a clean interface for components to consume. The hook should handle the following states:
loading: A boolean indicating whether the asynchronous function is currently executing.data: The result of the asynchronous function, if successful. Initiallyundefined.error: An error object if the asynchronous function throws an error. Initiallyundefined.execute: A function that can be called to manually trigger the execution of the asynchronous function.status: A string representing the current status of the async operation. Possible values: 'idle', 'pending', 'success', 'error'.
The hook should automatically execute the provided function when the component mounts (or when execute is called). It should update the loading, data, and error states accordingly. The execute function should reset the state to its initial values ('idle', undefined, undefined) before re-executing the async function.
Key Requirements:
- The hook must be generic, accepting a function that returns a Promise.
- The hook must handle errors gracefully and update the
errorstate. - The hook must provide a
loadingstate to indicate when the asynchronous function is running. - The hook must provide a
datastate to store the result of the asynchronous function. - The hook must provide an
executefunction to manually trigger the async function. - The hook must provide a
statusstate to indicate the current state of the async operation.
Expected Behavior:
- On initial mount, the
loadingstate should betrueand thedataanderrorstates should beundefined. Thestatusshould be 'pending'. - When the asynchronous function completes successfully, the
loadingstate should befalse, thedatastate should be updated with the result, theerrorstate should beundefined, and thestatusshould be 'success'. - If the asynchronous function throws an error, the
loadingstate should befalse, thedatastate should beundefined, theerrorstate should be updated with the error object, and thestatusshould be 'error'. - Calling the
executefunction should reset theloadingstate totrue, thedataanderrorstates toundefined, thestatusto 'pending', and then re-execute the asynchronous function.
Examples
Example 1:
// Async function (simulated API call)
async function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const success = true; // Simulate success or failure
if (success) {
resolve("Data fetched successfully!");
} else {
reject(new Error("Failed to fetch data."));
}
}, 1000);
});
}
// Usage in a component
import useAsync from './useAsync'; // Assuming you save the hook in useAsync.ts
function MyComponent() {
const { data, error, loading, execute, status } = useAsync(fetchData);
return (
<div>
<p>Status: {status}</p>
{loading && <p>Loading...</p>}
{error && <p>Error: {error.message}</p>}
{data && <p>Data: {data}</p>}
<button onClick={execute}>Fetch Data</button>
</div>
);
}
Output: Initially, "Loading..." is displayed. After 1 second, either "Data: Data fetched successfully!" or "Error: Failed to fetch data." is displayed, depending on the success variable in fetchData. Clicking "Fetch Data" restarts the process.
Example 2:
// Async function that throws an error
async function throwError() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error("Intentional error!"));
}, 500);
});
}
// Usage in a component
import useAsync from './useAsync';
function ErrorComponent() {
const { data, error, loading, execute, status } = useAsync(throwError);
return (
<div>
<p>Status: {status}</p>
{loading && <p>Loading...</p>}
{error && <p>Error: {error.message}</p>}
{data && <p>Data: {data}</p>}
<button onClick={execute}>Trigger Error</button>
</div>
);
}
Output: Initially, "Loading..." is displayed. After 500ms, "Error: Intentional error!" is displayed. Clicking "Trigger Error" restarts the process.
Constraints
- The hook must be written in TypeScript.
- The asynchronous function passed to the hook can return any type. The
datastate should be of typeT | undefined. - The
executefunction should not accept any arguments. - The hook should not rely on external libraries beyond React.
- The hook should be performant and avoid unnecessary re-renders.
Notes
- Consider using
useEffectto trigger the asynchronous function on mount and when theexecutefunction is called. - Think about how to handle potential race conditions if the component unmounts while the asynchronous function is still running. (Aborting the promise is not required for this challenge, but good to consider).
- The
statusstate is a helpful addition for debugging and displaying different states to the user. - Focus on creating a clean and reusable hook that can be easily integrated into different components.