Create asynchronous imperative forms of "asking" with ease, including implementations like confirm
or prompt
.
- 🛠️ Fully typesafe
- ✅ Headless, minimalist, bring your own UI
- 🪄 Works without a top-level context provider
To install the hook, run:
npm install use-ask
You can start the "ask" process by calling ask
or safeAsk
. These functions return a promise that resolves or rejects depending on whether ok
or cancel
is called.
Check out examples section for more examples using popular UI libraries.
Create a <Confirmer />
component using createAsk()
.
'use client'
import { useEffect, useRef } from 'react'
import { createAsk } from 'use-ask'
// The payload for this implementation is a basic message string.
// You can use an object as payload to render your confirm dialog.
const [confirmStore, useConfirmStore] = createAsk<string, boolean>()
export const confirm = confirmStore.ask
export const safeConfirm = confirmStore.safeAsk
export const Confirmer = () => {
const [{ key, payload: message }, { asking, cancel, ok }] = useConfirmStore()
const dialogRef = useRef<HTMLDialogElement>(null)
useEffect(() => {
if (asking) {
dialogRef.current?.showModal()
} else {
dialogRef.current?.close()
}
}, [asking])
return (
// We are using native <dialog> element for brevity, but you can customise the UI however you want.
<dialog key={key} ref={dialogRef} onCancel={cancel}>
<p>{message}</p>
<button onClick={cancel}>Cancel</button>
<button onClick={() => ok(true)}>OK</button>
</dialog>
)
}
Add <Confirmer />
to your app, it will be the place where your confirm dialog will be rendered. After that you can use confirm()
from anywhere in your app.
import { Confirmer, confirm } from '@/components/confirmer'
// ...
function App() {
return (
<div>
<Confirmer />
<button
onClick={() =>
confirm('Are you sure?')
.then(() => alert('Deleted!'))
.catch(() => alert('Cancelled!'))
}
>
Delete
</button>
</div>
)
}
You can also create a one-off use case by using useAsk
directly. Since the hook is headless, you can render a <Popover>
as well.
const Page = () => {
const [{ ask }, { asking, cancel, ok }] = useAsk<string, Error>() // We specify the data type to be `string`, and cancel reason to be `Error`.
return (
<Popover open={asking} onOpenChange={(open) => !open && cancel()}>
<PopoverPrimitive.Anchor asChild>
<Button
onClick={() =>
ask()
.then((message) => alert(`Confirmed with message "${message}"`))
// Error is `any` instead of `Error` by design. For more information, see https://github.com/Microsoft/TypeScript/issues/6283#issuecomment-240804072.
.catch((error) => alert(`Cancelled with message "${error.message}"`))
}
>
Delete
</Button>
</PopoverPrimitive.Anchor>
<PopoverContent side="right">
<div className="flex gap-2">
<Button
variant="outline"
className="w-full"
onClick={() => cancel(new Error('No good!'))}
>
Cancel
</Button>
<Button className="w-full" onClick={() => ok('All good!')}>
Continue
</Button>
</div>
</PopoverContent>
</Popover>
)
}
This library uses Promise.withResolvers under the hood, which is supported on most modern browsers. If you need to support older browsers, consider checking out ungap/with-resolvers.
- joy-ui-vite: Vite React app with confirmation dialog using Joy UI.
- shadcn-ui-nextjs: Nextjs app with confirmation alert dialog / popover using Shadcn UI.
- material-ui-confirm (Jonatan Kłosko) This library provided the initial inspiration for promisifying and creating an imperative method for confirm / prompt dialogs.
- sonner (Emil Kowalski) Inspired the use of the observer pattern for a global store, eliminating the need for a top-level provider.
- use-confirm (Daniil Tsivinsky)
This project influenced the naming
asking
, offering a more generic approach to implementation. - zod (Colin McDonnell)
Inspired the design of
ask
andsafeAsk
variants for error throwing and non-throwing APIs.