Implement a useAsync Hook for Asynchronous Operations in React
Asynchronous operations are fundamental to modern web applications, often involving fetching data from APIs, performing complex computations, or interacting with external services. Managing the loading, error, and success states of these operations within React components can become repetitive and verbose. Your task is to create a custom React hook, useAsync, that abstracts this common pattern, making it cleaner and more reusable.
Problem Description
You need to implement a TypeScript React hook named useAsync. This hook should simplify the management of asynchronous functions and their corresponding states (loading, error, and data).
The useAsync hook should:
- Accept an asynchronous function (a function that returns a Promise) as its primary argument.
- Accept an optional immediate execution flag. If
true, the async function should be executed immediately when the hook is first used. - Return an object containing:
data: The result of the asynchronous operation (initiallynullorundefined).error: Any error thrown during the operation (initiallynullorundefined).loading: A boolean indicating whether the asynchronous operation is currently in progress (initiallyfalse, ortrueif immediate execution is requested).run: A function that can be called to manually execute the asynchronous operation. This function should accept any arguments needed by the original async function.reset: A function to reset the hook's state to its initial values.
Key Requirements:
- The hook must correctly manage the
loadingstate, setting it totruebefore the promise starts andfalseafter it resolves or rejects. - It must capture the resolved
dataor theerrorthrown by the promise. - The
runfunction should allow re-execution of the async operation with new arguments. Subsequent calls torunshould resetdataanderrorand setloadingtotrue. - The hook should handle potential race conditions if
runis called multiple times quickly. Only the result of the latest initiated operation should be reflected in thedataorerrorstate. - The
resetfunction should revertdata,error, andloadingto their initial states.
Expected Behavior:
- Initial State: When
useAsyncis called without immediate execution,data,errorwill benull/undefined, andloadingwill befalse. - Immediate Execution: If
immediateistrue, the async function will run upon hook initialization, andloadingwill betrueuntil completion. - Manual Execution (
run): Callingrunwith arguments (if any) will setloadingtotrue, resetdataanderror, execute the async function, and updatedataorerrorupon completion, settingloadingtofalse. - Error Handling: If the async function throws an error, the
errorstate will be updated,datawill benull/undefined, andloadingwill befalse. - Race Condition Handling: If
runis called again while a previous operation is still pending, the results of the older operation should be ignored. - Resetting State (
reset): Callingresetshould restore the hook's state to its initial configuration.
Examples
Example 1: Basic Data Fetching
// Assume a utility function for fetching data
const fakeFetch = (url: string): Promise<string> =>
new Promise((resolve, reject) => {
setTimeout(() => {
if (url === "/data") {
resolve("Successfully fetched data!");
} else {
reject(new Error("Not Found"));
}
}, 500);
});
// In a React Component:
function MyComponent() {
const { data, error, loading, run } = useAsync(fakeFetch, false);
return (
<div>
<button onClick={() => run("/data")} disabled={loading}>
Fetch Data
</button>
{loading && <p>Loading...</p>}
{error && <p>Error: {error.message}</p>}
{data && <p>{data}</p>}
</div>
);
}
Input to useAsync: fakeFetch (async function), false (immediate execution flag).
Output (Initial): { data: undefined, error: undefined, loading: false, run: [function], reset: [function] }
Explanation: The hook is initialized, and the run function is available for manual invocation.
After clicking the "Fetch Data" button:
Output: { data: "Successfully fetched data!", error: undefined, loading: false, run: [function], reset: [function] }
Explanation: run("/data") was called. loading became true, fakeFetch resolved with the data, which was then stored in the data state, and loading became false.
Example 2: Immediate Execution with Error
const faultyFetch = (): Promise<string> =>
new Promise((_, reject) => {
setTimeout(() => {
reject(new Error("Something went wrong!"));
}, 500);
});
// In a React Component:
function AnotherComponent() {
const { data, error, loading } = useAsync(faultyFetch, true); // Immediate execution
return (
<div>
{loading && <p>Loading initially...</p>}
{error && <p>Initial Fetch Error: {error.message}</p>}
{data && <p>{data}</p>}
</div>
);
}
Input to useAsync: faultyFetch (async function), true (immediate execution flag).
Output (Initial/After ~500ms): { data: undefined, error: Error("Something went wrong!"), loading: false, run: [function], reset: [function] }
Explanation: The hook immediately executed faultyFetch. The promise rejected, and the error was captured. loading was true during execution and set to false afterward.
Example 3: Handling Race Conditions
const slowFetch = (id: number): Promise<string> =>
new Promise((resolve) => {
setTimeout(() => resolve(`Data for ${id}`), 1000);
});
// In a React Component:
function RaceComponent() {
const { data, error, loading, run } = useAsync(slowFetch, false);
const handleClick = () => {
run(1); // Start fetching for ID 1
setTimeout(() => {
run(2); // Immediately start fetching for ID 2 (should cancel the first)
}, 100);
};
return (
<div>
<button onClick={handleClick} disabled={loading}>
Fetch Sequentially (Race)
</button>
{loading && <p>Loading...</p>}
{error && <p>Error: {error.message}</p>}
{data && <p>{data}</p>}
</div>
);
}
Input to useAsync: slowFetch (async function), false.
Output (After clicking button): { data: "Data for 2", error: undefined, loading: false, run: [function], reset: [function] }
Explanation: The handleClick calls run(1). 100ms later, run(2) is called. Even though slowFetch(1) is still pending, the hook's internal mechanism should ensure that only the result of slowFetch(2) (which finishes later) is stored in the data state. The loading state would have been true during both operations and then false after the second one completed.
Constraints
- The
useAsynchook must be implemented in TypeScript. - The hook should not rely on any external state management libraries (e.g., Redux, Zustand).
- The hook should be efficient and avoid unnecessary re-renders.
- The asynchronous function passed to the hook must return a
Promise.
Notes
- Consider how to manage cancellation of promises to prevent race conditions. A common pattern is to use a flag or an AbortController if the underlying async operation supports it. For this challenge, you can simulate cancellation by simply ignoring results from older promises based on a unique identifier or a counter.
- Think about the initial state of
dataanderror.undefinedornullare common choices. - The
runfunction should be able to accept zero or more arguments, matching the signature of the provided asynchronous function. - Ensure proper type safety using TypeScript generics to infer the types of
dataanderror.