Hone logo
Hone
Problems

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. Initially undefined.
  • error: An error object if the asynchronous function throws an error. Initially undefined.
  • 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 error state.
  • The hook must provide a loading state to indicate when the asynchronous function is running.
  • The hook must provide a data state to store the result of the asynchronous function.
  • The hook must provide an execute function to manually trigger the async function.
  • The hook must provide a status state to indicate the current state of the async operation.

Expected Behavior:

  1. On initial mount, the loading state should be true and the data and error states should be undefined. The status should be 'pending'.
  2. When the asynchronous function completes successfully, the loading state should be false, the data state should be updated with the result, the error state should be undefined, and the status should be 'success'.
  3. If the asynchronous function throws an error, the loading state should be false, the data state should be undefined, the error state should be updated with the error object, and the status should be 'error'.
  4. Calling the execute function should reset the loading state to true, the data and error states to undefined, the status to '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 data state should be of type T | undefined.
  • The execute function 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 useEffect to trigger the asynchronous function on mount and when the execute function 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 status state 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.
Loading editor...
typescript