Skip to content
usefy

useAsync

Timing & async

Manage the full lifecycle of a single async task — object-style state, immediate auto-run, and AbortController cancellation.

Install

$ npm install @usefy/use-async

Quick start

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

function UserProfile({ id }: { id: string }) {
  const { data, error, isLoading, execute, reset } = useAsync(
    async (signal: AbortSignal, userId: string) => {
      const res = await fetch(`/api/user/${userId}`, { signal });
      if (!res.ok) throw new Error("Failed to load user");
      return (await res.json()) as { name: string };
    },
    { immediate: true, args: [id] }, // auto-load on mount with `id`
  );

  if (isLoading) return <p>Loading…</p>;
  if (error) return <button onClick={() => execute(id)}>Retry</button>;
  return (
    <div>
      <h1>{data?.name}</h1>
      <button onClick={reset}>Clear</button>
    </div>
  );
}

API reference

const { data, error, status, isLoading, execute, reset } = useAsync<T, Args, E>(fn, options?);

Parameters

ParameterTypeDescription
fn(signal: AbortSignal, ...args: Args) => Promise<T>The async function to run. Receives an AbortSignal first, then whatever args you pass to execute. Wire the signal into fetch(url, { signal }). Read through a ref — an inline function is fine and never goes stale.
optionsUseAsyncOptions<T, Args, E>Optional. See below.

Options — UseAsyncOptions<T, Args, E>

OptionTypeDefaultDescription
immediatebooleantrueAuto-run once on mount (client-side only, from an effect — never during SSR). Set false for a manual-only hook.
argsArgs[]Arguments for the immediate run. Required if your fn needs args and you keep immediate on. Ignored by manual execute(...) calls.
initialDataTSeed value for data before the first successful run. Status still starts "idle"; reset() restores it.
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 guarantees. The abort of a superseded/reset/unmounted call is never reported here.

Return — { data, error, status, isLoading, execute, reset }

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".
execute(...args: Args) => Promise<T | undefined>Runs fn(signal, ...args). Stable. Aborts any previous in-flight request first. Never rejects.
reset() => voidReturns state to idle (restoring initialData), aborts any in-flight request, and supersedes it. Stable.

Behavioural guarantees (by design)

  • AbortSignal signature: the signal is passed firstfn(signal, ...args) — keeping the forwarded Args tuple clean and fully inferable. Wire it into fetch(url, { signal }) (or any abortable API).
  • What execute resolves with: the value fn produced on success, or undefined on failure / supersession. execute never rejects — errors are surfaced via state.error, so a fire-and-forget execute() can never cause an unhandled rejection.
  • Data on error: the last successful data is kept (not cleared) when a later run fails. error is cleared the moment a new run starts.
  • Cancellation vs. stale-guard: aborting can't stop a plain promise, so both mechanisms run together — the AbortSignal cancels abortable work (like fetch), and a monotonic call-id guard guarantees a superseded call never updates state (or fires onSuccess/onError) even if it resolves late. An abort of a superseded/reset call is never surfaced as error.
  • immediate default: true. useAsync is the auto-running counterpart to the manual useAsyncFn — the reason to reach for it is a declarative "load on mount". If you want fully manual control, use useAsyncFn.
  • StrictMode: under React 18 StrictMode the mount effect double-invokes; the first auto-run's controller is aborted by the interleaved cleanup and the second run wins, so the double-mount is harmless.

Exported types

AsyncFnWithSignal<T, Args>, AsyncExecuteFn<T, Args>, UseAsyncOptions<T, Args, E>, UseAsyncReturn<T, Args, E>, plus the shared AsyncStatus, AsyncFn<T, Args>, AsyncState<T, E> re-exported from @usefy/use-async-fn.

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