useAsync
Timing & asyncManage the full lifecycle of a single async task — object-style state, immediate auto-run, and AbortController cancellation.
Install
$ npm install @usefy/use-asyncQuick 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
| Parameter | Type | Description |
|---|---|---|
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. |
options | UseAsyncOptions<T, Args, E> | Optional. See below. |
Options — UseAsyncOptions<T, Args, E>
| Option | Type | Default | Description |
|---|---|---|---|
immediate | boolean | true | Auto-run once on mount (client-side only, from an effect — never during SSR). Set false for a manual-only hook. |
args | Args | [] | Arguments for the immediate run. Required if your fn needs args and you keep immediate on. Ignored by manual execute(...) calls. |
initialData | T | — | Seed value for data before the first successful run. Status still starts "idle"; reset() restores it. |
onSuccess | (data: T) => void | — | Called 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) => void | — | Called 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 }
| Field | Type | Description |
|---|---|---|
data | T | undefined | The most recent successfully-resolved value. Retained across later pending/error transitions; only replaced on success. |
error | E | undefined | The 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. |
isLoading | boolean | Convenience 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 | () => void | Returns state to idle (restoring initialData), aborts any in-flight request, and supersedes it. Stable. |
Behavioural guarantees (by design)
- AbortSignal signature: the signal is passed first —
fn(signal, ...args)— keeping the forwardedArgstuple clean and fully inferable. Wire it intofetch(url, { signal })(or any abortable API). - What
executeresolves with: the valuefnproduced on success, orundefinedon failure / supersession.executenever rejects — errors are surfaced viastate.error, so a fire-and-forgetexecute()can never cause an unhandled rejection. - Data on error: the last successful
datais kept (not cleared) when a later run fails.erroris cleared the moment a new run starts. - Cancellation vs. stale-guard: aborting can't stop a plain promise, so both mechanisms run together — the
AbortSignalcancels abortable work (likefetch), and a monotonic call-id guard guarantees a superseded call never updates state (or firesonSuccess/onError) even if it resolves late. An abort of a superseded/reset call is never surfaced aserror. immediatedefault:true.useAsyncis the auto-running counterpart to the manualuseAsyncFn— the reason to reach for it is a declarative "load on mount". If you want fully manual control, useuseAsyncFn.- 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.