@void-snippets/react
useAsyncState
Generic async state machine with catchError-style execute.
#What it does
A lightweight state machine for any async operation. Tracks idle → pending → success | error and provides an execute() function that manages all transitions automatically. Use it for async operations that don't belong to a TanStack Query resource — file uploads, exports, one-off API calls.
#Signature
typescript
function useAsyncState<T>(initialData?: T | null): {
data: T | null;
status: 'idle' | 'pending' | 'success' | 'error';
error: Error | null;
isLoading: boolean;
isSuccess: boolean;
isError: boolean;
execute: (fn: () => Promise<T>, options?: { onSuccess?, onError? }) => Promise<[Error, null] | [null, T]>;
setData: (value: T) => void;
setError: (error: Error) => void;
reset: () => void;
}execute() returns a catchError-style tuple so you can act on the result inline without a separate try/catch.
#Example
tsx
function ExportPage() {
const { isLoading, isSuccess, isError, error, execute, reset } =
useAsyncState<{ downloadUrl: string }>();
const handleExport = async (format: 'csv' | 'xlsx') => {
const [err, result] = await execute(
() => ContactsApis.export({ format }),
{
onSuccess: () => toast.success('Export ready!'),
onError: (e) => toast.error(e.message),
},
);
if (result) window.open(result.downloadUrl, '_blank');
};
return (
<div>
{isError && <ErrorBanner message={error?.message}><Button onClick={reset}>Try again</Button></ErrorBanner>}
{isSuccess && <SuccessBanner>Your file is ready.</SuccessBanner>}
<Button onClick={() => handleExport('csv')} loading={isLoading}>Export CSV</Button>
<Button onClick={() => handleExport('xlsx')} loading={isLoading}>Export Excel</Button>
</div>
);
}