use Graft Alert
A question that suspends until it is answered: open() returns a Promise.
API
Returns
| prop | type | default | |
|---|---|---|---|
isActive? | boolean | — | whether the question is on screen |
storage? | T | null | — | the payload open was called with |
open? | (payload?: T) => Promise<boolean> | — | asks, and resolves with the answer |
close? | (result?: boolean) => void | — | answers with the result given |
onConfirm? | () => void | — | close(true) |
onDismiss? | () => void | — | close(false) |
id? | string | — | stable id, for aria-labelledby |
Usage
Ask, and wait
The deletion is written in one place, in the order it happens: ask, and if the answer is yes, delete. The row that started it is the row that finishes it — nothing is handed to the dialog and handed back, and the list has no idea a question was asked.
// -constants/graft.ts
export const GraftConfirm = createGraft({ name: "GraftConfirm", graft: useGraftAlert<string> });
// -components/file_list.tsx
export const FileList = () => {
const { list, remove } = GraftFiles.use();
const { open } = GraftConfirm.use();
const onDelete = async (file: string) => {
if (!(await open(file))) return;
remove(file);
};
return (
<List
array={list}
itemExtractor={({ row }) => <Item key={row} onClick={() => void onDelete(row)}>{row}</Item>}
/>
);
};
// -components/confirm_layer.tsx
export const ConfirmLayer = () => {
const { id, isActive, onConfirm, onDismiss, storage } = GraftConfirm.use();
return (
<Layer open={isActive} onClose={onDismiss} aria-labelledby={id}>
<Header.Title id={id} title={`Delete ${storage ?? "this file"}?`} />
<Button onClick={onDismiss}>cancel</Button>
<Button onClick={onConfirm}>delete</Button>
</Layer>
);
};The payload is what the question is about, so the dialog can name the file it is asking to delete without the caller passing it twice. Cancelling resolves false and the line after await simply never runs.
Details
The answer comes back to the caller
This is the same shape as a dialog with one difference, and the difference is the point: open() returns a Promise that resolves when the question is answered. The code that asked reads the answer where it asked, instead of splitting into a callback that has to be handed the rest of the operation.
Which is what makes a confirmation something you can insert into an existing flow without rewriting it. An action that used to close immediately gains a question in front of it and keeps its shape.
// -constants/graft_dialog.ts
export const GraftDialogInvoice = createGraft({ name: "GraftDialogInvoice", graft: useGraft });
function useGraft() {
const confirm = GraftConfirm.use();
const dialog = useGraftDialog<Invoice>();
const [isDirty, setIsDirty] = useState(false);
const close = useCallback(async () => {
if (isDirty && !(await confirm.open())) return;
dialog.close();
}, [confirm, dialog, isDirty]);
return { ...dialog, close, isDirty, setIsDirty };
}Every question is answered
A pending Promise that never settles is a leak you find months later, so there are none here. Asking again while a question is open resolves the previous one with false, and unmounting the provider does the same — an unanswered question always becomes a no.
close(result) answers with whatever you pass, and onConfirm and onDismiss are the two shorthands you end up wiring to buttons. Anything that dismisses without answering — Esc, the backdrop — should go to onDismiss, so the caller hears a no rather than nothing.