useAsyncAction
React hook to manage async operations with status, data, and error states.
Made by lucasidle
~65% success rate. Click upload to trigger the async action.
import useAsyncAction from "@/components/targetblank/hooks/use-async-action";
import { Button } from "@/components/ui/button";
function fakeUpload(): Promise<{ url: string }> {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.35) {
resolve({ url: "https://cdn.example.com/file-" + Date.now() });
} else {
reject(new Error("Upload failed — server error"));
}
}, 1400);
});
}
export default function AsyncActionDemo() {
const { execute, status, data, error, reset, isLoading } = useAsyncAction(
fakeUpload,
{ preventConcurrent: true },
);
return (
<div className="flex flex-col gap-4 w-[360px]">
<div className="flex items-center gap-3">
<span className="capitalize text-xs font-medium px-2 py-0.5 rounded-full border border-current">
{status}
</span>
{data && (
<span className="text-xs text-muted-foreground truncate">
{data.url}
</span>
)}
{error && (
<span className="text-xs text-destructive">{error.message}</span>
)}
</div>
<div className="flex gap-2">
<Button size="sm" onClick={() => execute()} disabled={isLoading}>
{isLoading ? "Uploading…" : "Upload file"}
</Button>
{status !== "idle" && (
<Button size="sm" variant="outline" onClick={reset}>
Reset
</Button>
)}
</div>
<p className="text-xs text-muted-foreground">
~65% success rate. Click upload to trigger the async action.
</p>
</div>
);
}Installation
Install the following dependencies:
Copy and paste the following code into your project:
import * as React from "react";
type AsyncStatus = "idle" | "loading" | "success" | "error";
interface UseAsyncActionOptions<TData> {
onSuccess?: (data: TData) => void;
onError?: (error: Error) => void;
onSettled?: (data: TData | undefined, error: Error | undefined) => void;
preventConcurrent?: boolean;
}
interface UseAsyncActionReturn<TData, TArgs extends unknown[]> {
execute: (...args: TArgs) => Promise<void>;
status: AsyncStatus;
data: TData | undefined;
error: Error | undefined;
reset: () => void;
isLoading: boolean;
isSuccess: boolean;
isError: boolean;
}
function useAsyncAction<TData, TArgs extends unknown[]>(
fn: (...args: TArgs) => Promise<TData>,
options?: UseAsyncActionOptions<TData>,
): UseAsyncActionReturn<TData, TArgs> {
const [status, setStatus] = React.useState<AsyncStatus>("idle");
const [data, setData] = React.useState<TData | undefined>(undefined);
const [error, setError] = React.useState<Error | undefined>(undefined);
const optionsRef = React.useRef(options);
optionsRef.current = options;
const execute = React.useCallback(
async (...args: TArgs) => {
if (optionsRef.current?.preventConcurrent && status === "loading") {
return;
}
setStatus("loading");
setError(undefined);
try {
const result = await fn(...args);
setData(result);
setStatus("success");
optionsRef.current?.onSuccess?.(result);
optionsRef.current?.onSettled?.(result, undefined);
} catch (err) {
const normalized = err instanceof Error ? err : new Error(String(err));
setError(normalized);
setStatus("error");
optionsRef.current?.onError?.(normalized);
optionsRef.current?.onSettled?.(undefined, normalized);
}
},
[fn, status],
);
const reset = React.useCallback(() => {
setStatus("idle");
setData(undefined);
setError(undefined);
}, []);
return {
execute,
status,
data,
error,
reset,
isLoading: status === "loading",
isSuccess: status === "success",
isError: status === "error",
};
}
export default useAsyncAction;Update the import paths to match your project setup.
Usage
const { execute, isLoading, isSuccess, isError, data, error, reset } =
useAsyncAction(async (payload) => {
return await saveSettings(payload);
});
await execute(values);Props
| Prop | Type | Default |
|---|---|---|
preventConcurrent? | boolean | - |
onSettled? | () => void | - |
onError? | (error: unknown) => void | - |
onSuccess? | (data: TData) => void | - |
fn | (...args: TArgs) => Promise<TData> | - |