Skip to content
usefy

useAsyncFn

Timing & async

Run a manual-trigger async function and track its idle/pending/success/error lifecycle — race-safe and unmount-safe.

Install

$ npm install @usefy/use-async-fn

Quick start

useAsyncFn.tsx
import { useAsyncFn } from "@usefy/use-async-fn";

function LoginForm() {
  const [state, run] = useAsyncFn(async (email: string, password: string) => {
    const res = await fetch("/api/login", {
      method: "POST",
      body: JSON.stringify({ email, password }),
    });
    if (!res.ok) throw new Error("Invalid credentials");
    return (await res.json()) as { token: string };
  });

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    run("me@example.com", "hunter2"); // fire-and-forget is safe — run never rejects
  };

  return (
    <form onSubmit={handleSubmit}>
      <button disabled={state.isLoading}>
        {state.isLoading ? "Signing in…" : "Sign in"}
      </button>
      {state.status === "error" && <p role="alert">{state.error?.message}</p>}
      {state.status === "success" && <p>Welcome! Token: {state.data?.token}</p>}
    </form>
  );
}

API reference

const [state, run] = useAsyncFn<T, Args, E>(fn, options?);

Parameters

ParameterTypeDescription
fn(...args: Args) => Promise<T>The async function to run. Receives whatever args you pass to run. Read through a ref — an inline function is fine and never goes stale.
optionsUseAsyncFnOptions<T, E>Optional. See below.

Options — UseAsyncFnOptions<T, E>

OptionTypeDescription
initialDataTSeed value for state.data before the first successful run. Status still starts "idle".
onSuccess(data: T) => voidCalled after a run resolves — only for the latest (non-superseded) run, only while mounted. Fired from the event turn, never inside a state updater.
onError(error: E) => voidCalled after a run fails — same latest-only, mounted-only, post-setState guarantees.

Return — [state, run]

state: AsyncState<T, E>

FieldTypeDescription
dataT | undefinedThe most recent successfully-resolved value. Retained across later pending/error transitions; only replaced on success.
errorE | undefinedThe error from the most recent failed run. Cleared when a run starts and when a run succeeds.
status"idle" | "pending" | "success" | "error"The lifecycle status — the source of truth.
isLoadingbooleanConvenience mirror of status === "pending".

run: (...args: Args) => Promise<T | undefined> — Stable across renders. Forwards args to fn, moves state to pending, then to success or error.

Behavioural guarantees (by design)

  • What run resolves with: the value fn produced for that specific call on success, or undefined on failure. run never rejects — errors are surfaced via state.error, so a fire-and-forget run() can never cause an unhandled promise rejection. (Because failure resolves to undefined, a T of undefined is ambiguous with failure — read state.error to disambiguate.)
  • Data on error: the last successful data is kept (not cleared) when a later run fails, so you can keep showing stale data alongside an error. error is cleared the moment a new run starts.
  • Race / stale-response guarding: each call gets a monotonically increasing id; if a newer run starts before an older one settles, the older result is ignored for state purposes (and its onSuccess/onError is skipped). Only the latest call wins. Each run() promise still resolves with its own result.
  • AbortController: intentionally not wired into fn's signature here, to keep the generic Args clean. In-flight results from superseded calls are discarded by the stale-guard rather than aborted. Abortable fetching is layered on by the higher-level useAsync/usePolling hooks.

Exported types

AsyncStatus, AsyncFn<T, Args>, AsyncState<T, E>, AsyncRunFn<T, Args>, UseAsyncFnOptions<T, E>, UseAsyncFnReturn<T, Args, E>.

Go deeper

This page is the quick reference. For every example, prop, and edge case, read the full README — or open Storybook to change props live.

More in timing & async