useAsyncHandler is a hook that automatically executes an asynchronous function on mount and every time its dependency list (deps) changes. It provides the current execution state through status (isPending, isSuccess, isError, error). Internally, it uses useAsyncCallback, so status updates are lazy — re-renders start only after the first time you read status.
The hook returns a hybrid structure that supports both tuple and object destructuring:
- Tuple:
[status] - Object:
{ status }
function useAsyncHandler(
handler: UseAsyncHandlerFunction,
deps: React.DependencyList,
): UseAsyncHandlerReturn;-
Parameters
handler— an asynchronous function without arguments that will be executed automatically.deps— a dependency array (like in the second argument ofuseEffect); whenever dependencies change,handleris executed again.
-
Returns:
UseAsyncHandlerReturn— a hybrid structure containing:status— includesisPending,isSuccess,isError, anderror.
import { useAsyncHandler } from '@webeach/react-hooks/useAsyncHandler';
export function DataLoader() {
const { status } = useAsyncHandler(async () => {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error('Network error');
}
const result = await response.json();
console.log(result);
}, []);
if (status.isPending) {
return <div>Loading…</div>;
}
if (status.isError) {
return <div role="alert">{status.error?.message}</div>;
}
return <div>Done</div>;
}import { useAsyncHandler } from '@webeach/react-hooks/useAsyncHandler';
type SearchProps = {
query: string;
page: number;
};
export function Search(props: SearchProps) {
const { query, page } = props;
const { status } = useAsyncHandler(async () => {
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}&page=${page}`);
if (!res.ok) {
throw new Error('Failed to load');
}
const result = await res.json();
console.log(result);
}, [query, page]);
return (
<div>
{status.isPending && <span>Loading…</span>}
{status.isError && <span role="alert">{status.error?.message}</span>}
</div>
);
}-
Automatic execution on
deps- Runs
handleron mount and every timedepschange. Ifhandlerthrows an error,statusbecomes'error'.
- Runs
-
Lazy reactivity
- Status updates don’t trigger re-renders until you actually read
status. Once read, reactivity is enabled for future changes.
- Status updates don’t trigger re-renders until you actually read
-
Cleanup and cancellation
- On unmount, the current execution is logically canceled (only the latest call can update the status). This prevents race conditions and updates after unmount.
-
Development logs
- In development mode, errors thrown inside
handlerare also logged to the console.
- In development mode, errors thrown inside
-
Hybrid return structure
- Can be used as a tuple
[status]or as an object{ status }.
- Can be used as a tuple
-
No manual control
- This hook does not provide
abort()orhandler()to call manually. If you need explicit control, useuseAsyncCallbackinstead.
- This hook does not provide
- Automatically trigger async operations on mount and when dependencies change.
- Pages or widgets that need to fetch data whenever route/locale/filter params change.
- Simple flows: request → loading → success/error without manual retries.
- You need manual triggering, cancellation, or retries — use
useAsyncCallbackinstead. - You need advanced dependency comparison — use
useEffectCompareor manage dependencies manually. - You need full-featured data-fetching management (caching, refetch policies, pagination) — consider React Query or similar libraries.
-
Missing dependencies
- Forgetting a dependency in
depscan lead to stale results.
- Forgetting a dependency in
-
Unstable dependencies
- Passing a new object/function on every render will cause unnecessary re-runs. Use
useMemo/useCallback.
- Passing a new object/function on every render will cause unnecessary re-runs. Use
-
Expecting manual control
useAsyncHandleris fully automatic. It doesn’t exposehandler()orabort().
-
Not handling errors
- Always handle
status.isErrorand usestatus.error?.messagein your UI. In development, errors are also logged to the console, but you should not rely on that.
- Always handle
Exported types
-
UseAsyncHandlerFunction- Asynchronous function without arguments:
() => Promise<void>.
- Asynchronous function without arguments:
-
UseAsyncHandlerReturn- Hybrid return type: tuple
[status]and object{ status }.
- Hybrid return type: tuple
-
UseAsyncHandlerReturnObject- Object form:
{ status: StatusStateMapTuple & StatusStateMap }.
- Object form:
-
UseAsyncHandlerReturnTuple- Tuple form:
[status: StatusStateMapTuple & StatusStateMap].
- Tuple form: