Promise Toasts
Loading → success → error. The state machine you were too busy to write.
toast.promise() handles the loading → success → error dance. Hand it a promise and three messages. Popser does the rest.
import { toast } from "@vcui/popser";
toast.promise(fetchData(), {
loading: "Fetching data...",
success: "Data loaded",
error: "Failed to fetch",
});Signature
toast.promise<T>(
promiseOrFn: Promise<T> | (() => Promise<T>),
options: PopserPromiseOptions<T>
): Promise<T> & { id: string }Returns the original promise with an id tacked on. For tracking, if you're into that.
Options
{
loading: ReactNode;
success: ReactNode | ((result: T) => ReactNode | PopserPromiseExtendedResult | undefined);
error: ReactNode | ((error: unknown) => ReactNode | PopserPromiseExtendedResult | undefined);
description?: ReactNode | ((data: T) => ReactNode) | ((error: unknown) => ReactNode);
finally?: () => void | Promise<void>;
id?: string;
signal?: AbortSignal;
aborted?: ReactNode | ((reason: unknown) => ReactNode | PopserPromiseExtendedResult | undefined);
onAbort?: (reason: unknown) => void;
}Dynamic messages
Pass a function to success or error and compute from the actual result. Groundbreaking, truly.
toast.promise(saveUser(data), {
loading: "Saving...",
success: (user) => `Saved ${user.name}`,
error: (err) => `Failed: ${err.message}`,
});JSX results
Handlers take ReactNode. Strings, JSX, whatever. Go mental.
toast.promise(saveUser(data), {
loading: "Saving...",
success: (user) => <span>Saved <strong>{user.name}</strong></span>,
error: (err) => <span className="text-red-500">{err.message}</span>,
});Extended results
Return an object with title to take full control. Description, timeout, icon, actions — the whole menu.
toast.promise(uploadFile(file), {
loading: "Uploading...",
success: (result) => ({
title: "Uploaded",
description: result.filename,
timeout: 8000,
icon: <FileIcon />,
action: {
label: "View",
onClick: () => openFile(result.url),
},
}),
error: (err) => ({
title: "Upload failed",
description: err.message,
timeout: 0, // stays until dismissed
}),
});Detected by the presence of title (and not being a React element). Any PopserOptions field works here.
Conditional skip
Return undefined to silently dismiss instead of showing a result. For background operations nobody asked to see.
toast.promise(fetchData(), {
loading: "Loading...",
success: (data) => data.silent ? undefined : "Done!",
error: (err) => err.name === "AbortError" ? undefined : `Error: ${err.message}`,
});Suppressing abort errors? Chef's kiss.
Per-state descriptions
description can be a function. Gets the resolved data or the error, depending on how things went.
toast.promise(fetchItems(), {
loading: "Loading items...",
success: "Done",
description: (items) => `${items.length} items loaded`,
error: "Failed",
});For errors:
toast.promise(save(), {
loading: "Saving...",
success: "Saved",
error: "Failed",
description: (err) => err instanceof Error ? err.message : "Unknown error",
});Finally
Runs after the promise settles. Success, error, doesn't matter. Cleanup is cleanup.
toast.promise(uploadFile(file), {
loading: "Uploading...",
success: "Done",
error: "Failed",
finally: () => {
cleanup();
},
});Lazy promises
Pass a function instead of a promise. Doesn't fire until the toast is created.
toast.promise(
() => fetchData(),
{
loading: "Loading...",
success: "Done",
error: "Failed",
},
);Handy when you want the loading toast to appear before the request fires. Timing matters.
AbortSignal
Cancel in-flight promise toasts. Pass a signal and optionally aborted content.
const controller = new AbortController();
toast.promise(fetchData(), {
loading: "Fetching...",
success: "Done",
error: "Failed",
signal: controller.signal,
aborted: "Cancelled",
});
// later
controller.abort();
// toast updates to "Cancelled" with type "warning"Abort content options
aborted follows the same pattern as success/error:
// Static message
aborted: "Cancelled"
// Dynamic from abort reason
aborted: (reason) => `Cancelled: ${reason}`
// Extended result with full control
aborted: (reason) => ({
title: "Aborted",
description: String(reason),
timeout: 5000,
icon: <StopIcon />,
})
// Return undefined to silently dismiss
aborted: (reason) => reason === "user" ? "Cancelled" : undefined
// Omit entirely to auto-dismiss on abortDefault type for aborted state: "warning".
onAbort callback
Fires before the toast updates. Use for cleanup.
toast.promise(uploadFile(file), {
loading: "Uploading...",
success: "Uploaded",
error: "Failed",
signal: controller.signal,
aborted: "Upload cancelled",
onAbort: (reason) => {
cleanupPartialUpload();
},
});If the promise resolves or rejects before the signal fires, the abort is a no-op. If the signal is already aborted when toast.promise() is called, abort handling runs immediately.
Custom ID
Pin a specific ID:
const result = toast.promise(save(), {
loading: "Saving...",
success: "Saved",
error: "Failed",
id: "save-operation",
});
console.log(result.id); // "save-operation"