Skip to content

useAsyncAction ​

One step a surface waits on. The button goes quiet while it runs, and the reason stays on screen when it fails.

tsx
import { useAsyncAction } from '@basmilius/react-ui';
tsx
import { Button, FormError, useAsyncAction } from '@basmilius/react-ui';

const wait = (ms: number): Promise<void> => new Promise((resolve) => window.setTimeout(resolve, ms));

let attempts = 0;

async function publish(): Promise<void> {
    await wait(900);
    attempts += 1;
    if (attempts % 2 === 1) {
        throw new Error('The registry refused the upload: the version already exists.');
    }
}

export default function UseAsyncActionDemo() {
    const step = useAsyncAction('Publishing failed.');

    return (
        <div className="flex w-80 flex-col items-start gap-2">
            <Button variant="primary" disabled={step.busy} onClick={() => void step.run(publish)}>
                {step.busy ? 'Publishing...' : 'Publish'}
            </Button>
            {step.failure !== null && <FormError>{step.failure}</FormError>}
        </div>
    );
}

The first press fails, the second goes through. run(work) clears the last failure, sets busy, awaits the work, and answers whether it went through, so the caller closes a dialog or steps on only on success:

tsx
const step = useAsyncAction();

async function save(): Promise<void> {
    if (await step.run(() => api.save(draft))) {
        onClose();
    }
}

A rejection becomes failure, the error's message through messageOf. The fallback you pass the hook names the step when the rejection carries no sentence of its own, such as a thrown string.

What it answers ​

MemberType
busybooleanTrue while run awaits.
failurestring | nullThe reason the last run failed.
run(work)(work: () => Promise<unknown>) => Promise<boolean>
fail(message)(message: string) => voidA refusal the surface sees for itself, shown in the same line as one that came back.
clear()() => void

PromptDialog runs its confirm through this hook. AsyncAction is an exported type.