useAsyncCallback is a hook wrapper around an asynchronous function. It tracks its execution status ('pending' / 'success' / 'error'), supports lazy reactivity (re-renders only start after status is read for the first time), and protects against race conditions: only the latest call can update the status. It provides methods: handler (to invoke the async operation) and abort() (to cancel/reset the current call).
The return value is a hybrid that works with both tuple and object destructuring: [handler, status, abort] and { handler, status, abort }.
function useAsyncCallback<Args extends unknown[], R>(
asyncCallback: (...args: Args) => Promise<R>,
): UseAsyncCallbackReturn<Args, R>;-
Parameters
asyncCallback— the async function whose execution status should be tracked.
-
Returns:
UseAsyncCallbackReturn<Args, R>— a hybrid structure:handler(...args): Promise<R>— a wrapper around the original async function.status— flagsisPending,isSuccess,isError, anderror(present only when an error occurs).abort()— logically cancels the current call and resets the status to'initial'.
import { useAsyncCallback } from '@webeach/react-hooks/useAsyncCallback';
type SaveButtonProps = {
save: () => Promise<void>;
};
export function SaveButton(props: SaveButtonProps) {
const { save } = props;
**
- const { handler: onSave, status, abort } = useAsyncCallback(save);
return (
<>
<button onClick={() => onSave()} disabled={status.isPending}>
{status.isPending ? 'Saving…' : 'Save'}
</button>
{status.isError && status.error !== null && (
<div role="alert">{status.error.message}</div>
)}
{/* Optional: let the user cancel */}
{status.isPending && (
<button onClick={abort} type="button">Cancel</button>
)}
</>
);
}const [submit] = useAsyncCallback(async (form: FormData) => {
await api.submit(form);
});
// The component will NOT re-render on status changes,
// since `status` is never read. Use await/try-catch if needed.import { ChangeEventHandler } from 'react';
import { useAsyncCallback } from '@webeach/react-hooks/useAsyncCallback';
export function SearchBox() {
const { handler: load, status, abort } = useAsyncCallback(async (q: string) => {
const response = await fetch(`/api/search?q=${encodeURIComponent(q)}`);
if (!response.ok) {
throw new Error('Network error');
}
return response.json();
});
const handleInputChange: ChangeEventHandler<HTMLInputElement> = (event) => {
abort(); // logically cancel the previous request
void load(event.target.value); // start a new one
};
return (
<div>
<input onChange={handleInputChange} placeholder="Search…"/>
{status.isPending && <span>Loading…</span>}
{status.isError && <span role="alert">{status.error?.message}</span>}
</div>
);
}-
Lazy reactivity
- Until you actually read the
statusfield, status changes do not trigger re-renders. Oncestatusis read, reactivity is enabled and subsequent updates will reflect in the UI.
- Until you actually read the
-
Race condition safety
- If
handleris called again before the previous call finishes, only the latest call can update the status. Results from outdated calls are ignored.
- If
-
Statuses and error
- Provides flags:
isPending,isSuccess,isError, anderror(ErrorLike | null). If a non-ErrorLikevalue is thrown, the hook attempts to convert it into anError(e.g. astring→Error).
- Provides flags:
-
abort()- Resets the status back to
'initial'and marks the current call as canceled. It does not physically cancel network requests — useAbortControllerinsideasyncCallbackfor real I/O cancellation.
- Resets the status back to
-
Unmount behavior
- On component unmount,
abort()is called automatically to prevent race conditions and state leaks.
- On component unmount,
-
Hybrid return structure
- The return value can be used both as a tuple
[handler, status, abort]and as an object{ handler, status, abort }— pick whichever fits your code style.
- The return value can be used both as a tuple
- Wrapping async requests/operations with built‑in status tracking.
- Disabling UI while a request is pending (
disabled={status.isPending}), and showing success or error states. - Scenarios where race condition protection matters (only the last call affects state).
- If plain
useState+useCallbackis enough without status tracking. - If you rely on external state/data fetching libraries (e.g. React Query) — prefer their built‑in features.
- If you need true low‑level cancellation of network requests — implement
AbortControllerinside yourasyncCallback.
-
Expecting re-renders without reading
status- If you never use
status, the component won’t re-render on updates. Either consumestatusor handle async flow viaawait/try-catch.
- If you never use
-
Ignoring thrown errors
- The wrapped
handlerrethrows any error. Always usetry/catchif you don’t rely onstatus.isError.
- The wrapped
-
Misunderstanding
abort()abort()only resets internal state and ignores outdated results — it does not cancel the underlying fetch. UseAbortControllerif cancellation is required.
-
Starting overlapping calls unintentionally
- Since only the latest call updates state, launching multiple calls simultaneously may hide earlier results.
Exported types
-
UseAsyncCallbackReturn<Args, ReturnType>**- Hybrid return type: tuple
[handler, status, abort]and object{ handler, status, abort }.
- Hybrid return type: tuple
-
UseAsyncCallbackReturnObject<Args, ReturnType>**- Object form with fields
handler,status,abort.
- Object form with fields
-
UseAsyncCallbackReturnTuple<Args, ReturnType>**- Tuple form:
[handler: (...args: Args) => Promise<R>, status: StatusStateMapTuple & StatusStateMap, abort: () => void].
- Tuple form:
-
UseAsyncCallbackAbortHandler**- Function signature for cancellation:
() => void.
- Function signature for cancellation: