useAsyncFn
Timing & asyncRun a manual-trigger async function and track its idle/pending/success/error lifecycle — race-safe and unmount-safe.
Install
$ npm install @usefy/use-async-fnQuick start
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
| Parameter | Type | Description |
|---|---|---|
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. |
options | UseAsyncFnOptions<T, E> | Optional. See below. |
Options — UseAsyncFnOptions<T, E>
| Option | Type | Description |
|---|---|---|
initialData | T | Seed value for state.data before the first successful run. Status still starts "idle". |
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, post-setState guarantees. |
Return — [state, run]
state: AsyncState<T, E>
| 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". |
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
runresolves with: the valuefnproduced for that specific call on success, orundefinedon failure.runnever rejects — errors are surfaced viastate.error, so a fire-and-forgetrun()can never cause an unhandled promise rejection. (Because failure resolves toundefined, aTofundefinedis ambiguous with failure — readstate.errorto disambiguate.) - Data on error: the last successful
datais kept (not cleared) when a later run fails, so you can keep showing stale data alongside an error.erroris cleared the moment a new run starts. - Race / stale-response guarding: each call gets a monotonically increasing id; if a newer
runstarts before an older one settles, the older result is ignored for state purposes (and itsonSuccess/onErroris skipped). Only the latest call wins. Eachrun()promise still resolves with its own result. - AbortController: intentionally not wired into
fn's signature here, to keep the genericArgsclean. In-flight results from superseded calls are discarded by the stale-guard rather than aborted. Abortable fetching is layered on by the higher-leveluseAsync/usePollinghooks.
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.