UI Bloom

useAsyncRunner

A custom React hook to execute async functions with built-in state management for loading, success, failure, data, and error states—all fully type-safe.

How To Use

Install

Run the following command in your terminal to add the hook to your project:

Import

Import the hook in the file where you want to use it:

import { useAsyncRunner } from '@/hooks/useAsyncRunner';

Setup

  1. Define your asynchronous function (action).
  2. Pass it to the hook via the action parameter.
  3. Destructure run, isPending, isSuccess, isFailure, data, and error from the hook.

Features

  • Built-in state flags: isPending, isSuccess, isFailure
  • Automatically captures data and error
  • Fully generic and type-safe
  • Reset state on each run
  • Lightweight and SSR-compatible

Always pass a stable action reference (e.g. useCallback) to avoid unnecessary re-renders.

API

type AsyncCallback<Args extends unknown[] = [], Return = void> = (
...args: Args
) => Promise<Return>;
 
interface UseAsyncRunnerParams<Args extends unknown[], Return> {
action: AsyncCallback<Args, Return>;
}
 
declare function useAsyncRunner<Args extends unknown[], Return>(
params: UseAsyncRunnerParams<Args, Return>
): {
/** Execute the async action */
run: (...args: Args) => Promise<Return | undefined>;
/** True while the action is running _/
isPending: boolean;
/** True when the action resolves successfully _/
isSuccess: boolean;
/** True when the action throws an error */
isFailure: boolean;
/** The resolved value (or null before run) _/
data: Return | null;
/** The caught error (or null before run) _/
error: unknown;
};

Example

Below is a complete example that fetches a post by ID using JSONPlaceholder and demonstrates how to handle loading, success, and error states.

import React, { useState } from 'react';
import useAsyncRunner from '@/hooks/useAsyncRunner';
 
type Post = {
userId: number;
id: number;
title: string;
body: string;
};
 
const fetchPostById = async (id: number): Promise<Post> => {
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
if (!res.ok) throw new Error('Failed to fetch post');
return res.json();
};
 
export default function FetchPost() {
const [postId, setPostId] = useState(1);
 
  const {
    run: fetchPost,
    data,
    error,
    isPending,
    isSuccess,
    isFailure,
  } = useAsyncRunner<[number], Post>({ action: fetchPostById });
 
  return (
    <div className="space-y-4 p-4">
      <h1 className="text-xl font-bold">Fetch Post</h1>
 
      <div>
        <label className="mr-2">Post ID:</label>
        <input
          type="number"
          value={postId}
          onChange={(e) => setPostId(Number(e.target.value))}
          className="rounded border px-2 py-1"
        />
        <button
          onClick={() => fetchPost(postId)}
          className="ml-2 rounded bg-blue-600 px-4 py-1 text-white"
        >
          Fetch
        </button>
      </div>
 
      {isPending && <p>Loading...</p>}
      {isFailure && <p className="text-red-500">Error: {String(error)}</p>}
      {isSuccess && data && (
        <div className="rounded border p-2">
          <h2 className="font-semibold">Title: {data.title}</h2>
          <p>{data.body}</p>
        </div>
      )}
    </div>
  );
 
}

On this page