Appearance
useConfirmation
What this solves
Confirmation UX is easy to get wrong when it’s implemented ad hoc:
- Multiple dialogs with inconsistent copy/labels
- Async flows that continue even when the user cancels
- State scattered across buttons, modals, and callbacks
useConfirmation centralizes the confirmation flow with a simple promise-based API: call confirm(...), await a boolean, and render one dialog bound to the hook state.
When to use it
Use useConfirmation for:
- Destructive actions (delete, remove access, archive)
- Risky actions (reset, overwrite, bulk operations)
- Any flow where you want a single, consistent confirm dialog
When NOT to use it
Do not use useConfirmation when:
- The decision is part of a larger multi-step flow (use a dedicated modal/wizard)
- You need multiple simultaneous confirmations (this hook models a single active confirmation at a time)
- You need to collect additional input beyond confirm/cancel (build a custom dialog)
Basic usage
Render the dialog once, and await confirm(...) from actions.
tsx
import { useConfirmation } from '@/shared/hooks'
import { ConfirmDialog } from '@/shared/ui/components/ConfirmDialog'
export function Example() {
const { state, confirm, handleConfirm, handleCancel } = useConfirmation()
const handleDelete = async () => {
const ok = await confirm({
title: 'Delete item?',
description: 'This action cannot be undone.',
confirmLabel: 'Delete',
cancelLabel: 'Cancel',
variant: 'destructive',
})
if (!ok) return
// await api.item.delete()
}
return (
<>
<button type="button" onClick={handleDelete}>Delete</button>
<ConfirmDialog
open={state.isOpen}
options={state.options}
onConfirm={handleConfirm}
onCancel={handleCancel}
/>
</>
)
}Common patterns
Pattern 1 — Page-level confirmation (single source of truth)
Keep the confirmation hook at the page/container level and pass confirm to children that trigger risky actions.
tsx
import { useConfirmation } from '@/shared/hooks'
import { ConfirmDialog } from '@/shared/ui/components/ConfirmDialog'
function DeleteButton({ onRequestDelete }: { onRequestDelete: () => Promise<void> }) {
return <button type="button" onClick={onRequestDelete}>Delete</button>
}
export function Page() {
const { state, confirm, handleConfirm, handleCancel } = useConfirmation()
const requestDelete = async () => {
const ok = await confirm({ title: 'Delete?', variant: 'destructive' })
if (!ok) return
// await api.delete()
}
return (
<>
<DeleteButton onRequestDelete={requestDelete} />
<ConfirmDialog open={state.isOpen} options={state.options} onConfirm={handleConfirm} onCancel={handleCancel} />
</>
)
}Why this is recommended:
- One dialog instance for the page
- Children stay stateless and reusable
Pattern 2 — Compose with useAsyncAction for the destructive operation
Use confirmation to gate the action, then let useAsyncAction handle the operation state.
tsx
import { useAsyncAction, useConfirmation } from '@/shared/hooks'
import { ConfirmDialog } from '@/shared/ui/components/ConfirmDialog'
export function DeleteUser() {
const { state, confirm, handleConfirm, handleCancel } = useConfirmation()
const remove = useAsyncAction(async () => {
// await api.user.delete()
return true
})
const onClick = async () => {
const ok = await confirm({ title: 'Delete user?', variant: 'destructive' })
if (!ok) return
await remove.run()
}
return (
<>
<button type="button" onClick={onClick} disabled={remove.isLoading}>
{remove.isLoading ? 'Deleting…' : 'Delete'}
</button>
<ConfirmDialog open={state.isOpen} options={state.options} onConfirm={handleConfirm} onCancel={handleCancel} />
</>
)
}Things to avoid
- Rendering multiple
ConfirmDialoginstances for the same confirmation state - Calling
confirm(...)without ensuring a dialog is rendered (the promise will never resolve) - Putting domain side effects inside the dialog component; keep them in the action handler that awaits the boolean
Related primitives
API reference (concise)
ts
export interface ConfirmOptions {
title: string
description?: string
confirmLabel?: string
cancelLabel?: string
variant?: 'default' | 'destructive'
}
export interface ConfirmationState {
isOpen: boolean
options: ConfirmOptions | null
}
export interface UseConfirmationReturn {
state: ConfirmationState
confirm: (options: ConfirmOptions) => Promise<boolean>
handleConfirm: () => void
handleCancel: () => void
}
export function useConfirmation(): UseConfirmationReturnFinal notes
confirm(...)is promise-based; the promise resolves only whenhandleConfirm()orhandleCancel()is called (typically fromConfirmDialog).- Keep confirmation copy short and explicit; prefer
variant: 'destructive'for irreversible actions.