@usefy/use-async
v0.25.1
Published
A React hook for the full lifecycle of a single async task — object-style state, immediate auto-run, and AbortController cancellation
Downloads
1,467
Maintainers
Readme
Overview
useAsync is part of the @usefy ecosystem — a collection of production-ready, TypeScript-first, SSR-safe React hooks. It manages the full lifecycle of a single async task (idle → pending → success | error) with an object-style return, built-in AbortController cancellation, and an immediate auto-run on mount.
It is the abortable, self-running sibling of useAsyncFn — same state shape, but it runs itself on mount, hands your function an AbortSignal so obsolete requests are truly cancelled, and can be reset() back to idle.
It is deliberately not a query cache — no keys, no cross-component dedupe, no background revalidation. For that reach for TanStack Query; useAsync is a focused local-async primitive.
Features
- Object-style state —
{ data, error, status, isLoading }wherestatus(idle/pending/success/error) is the source of truth andisLoadingmirrorspending. Identical in meaning touseAsyncFn. - AbortController cancellation — every
executecreates a freshAbortControllerand passes itssignalas the first argument to your function; the previous request is aborted when a newexecutestarts, onreset(), and on unmount immediateauto-run — runs once on mount by default (opt out withimmediate: false), fed byoptions.args; fires from an effect, so it never runs during SSR render- Race-safe — a superseded call can never update state (or fire callbacks) even if it resolves after being aborted — a monotonic call-id guard backs up the abort
- Unmount-safe — the in-flight request is aborted and no state update / callback runs after unmount
- Stable
execute/reset— memoized identities, safe as effect deps or child props; read the latest inlinefnthrough a ref so you never need to memoize it - Never rejects —
executeresolves with the value (orundefinedon failure) so an un-awaited call can't throw; errors surface viastate.error - SSR-safe & StrictMode-safe — touches no browser globals at module/render time; under StrictMode the first auto-run is aborted and the second wins
- TypeScript-first — full type inference; reuses the shared
AsyncStatus/AsyncState/AsyncFntypes - Tiny & tree-shakeable — published as its own package
Installation
# npm
npm install @usefy/use-async
# yarn
yarn add @usefy/use-async
# pnpm
pnpm add @usefy/use-asyncRequires React 18 or 19 (peerDependencies: "react": "^18.0.0 || ^19.0.0").
Quick Start
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
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.
Testing
📊 View Detailed Coverage Report (GitHub Pages) — 28 tests, 100% statement coverage.
License
MIT © mirunamu
This package is part of the usefy monorepo.
