Dialogs
Modal dialogs and prompts
Always use the usePrompt() hook for modal dialogs and confirmations. Do NOT use raw Dialog or Drawer components directly.
Basic Usage
import { usePrompt } from "@/forms/prompt";
function MyComponent() {
const prompt = usePrompt();
const handleDelete = () => {
prompt({
title: "Delete Item",
description: "Are you sure you want to delete this item?",
action: async () => {
return deleteItem(id);
},
buttonProps: {
variant: "destructive",
icon: "trash",
i18nButtonKey: "delete",
},
});
};
return <Button onClick={handleDelete}>Delete</Button>;
}Auto-Close Behavior
The prompt automatically closes when the action callback returns { success: true } (matching the ActionResponse type).
const handleApprove = () => {
prompt({
action: async () => {
// Must await and return the mutation result
return await updateStatusMutation.mutateAsync({
id: requestId,
status: "active",
});
},
title: t("approve.title"),
buttonProps: {
variant: "primary",
icon: "check",
i18nButtonKey: "approve",
},
});
};Important
The action must return the result. The prompt checks for { success: true } to know when to close.
Prompt Options
| Option | Type | Description |
|---|---|---|
title | ReactNode | Dialog title |
description | ReactNode | Dialog description |
icon | IconKeys | Icon to show in title |
action | Function | Async function to execute on confirm |
buttonProps | Object | Props for confirm button (variant, icon, i18nButtonKey) |
children | ReactNode | Custom content for dialog body |
form | UseFormReturn | react-hook-form instance for form dialogs |
customActions | Function | Custom action buttons renderer |
With Form Content
const handleChangeRole = (member: Member) => {
prompt({
title: t("members.actions.changeRole"),
action: async () => {
return updateMemberRole({
memberId: member.id,
role: selectedRoleRef.current,
});
},
children: (
<Select
defaultValue={member.role}
onValueChange={(value) => {
selectedRoleRef.current = value;
}}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{roles.map((role) => (
<SelectItem key={role} value={role}>
{t(`roles.${role}`)}
</SelectItem>
))}
</SelectContent>
</Select>
),
buttonProps: {
variant: "primary",
icon: "edit",
i18nButtonKey: "update",
},
});
};With react-hook-form
const { form, execute } = useAuthDeleteAccount();
const prompt = usePrompt();
const handlePromptDelete = () => {
prompt({
title: t("confirmTitle"),
description: t("confirmDescription"),
action: execute,
form: form,
buttonProps: {
variant: "destructive",
icon: "trash",
i18nButtonKey: "delete",
},
children: (
<FormField
control={form.control}
name="confirmation"
render={({ field }) => (
<FormItem>
<FormLabel>{t("confirmation.label")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
),
});
};Form Re-rendering
Critical
When using form fields inside prompt children, use useWatch instead of form.watch(). The prompt content is rendered outside the normal component tree, so form.watch() won't trigger re-renders correctly.
// ❌ Won't re-render in prompt children
const value = form.watch("fieldName");
// ✅ Will re-render correctly
import { useWatch } from "react-hook-form";
const value = useWatch({ control: form.control, name: "fieldName" });Custom Actions
For complex dialogs with multiple actions:
prompt({
title: "Choose Action",
customActions: (close) => (
<div className="flex gap-2">
<Button variant="outline" onClick={close}>
Cancel
</Button>
<Button variant="secondary" onClick={() => handleSave(false)}>
Save Draft
</Button>
<Button variant="primary" onClick={() => handleSave(true)}>
Publish
</Button>
</div>
),
children: <MyFormContent />,
});Best Practices
Never use raw Dialog/Drawer components for confirmations
The action must return { success: true } for auto-close
Match button variant to action severity
For proper re-rendering inside prompt children