@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>
  );
}