Your component can await.
createCallable() turns any React component into something you can await. Confirmations, dialogs, toasts, pickers, menus — any UI that conceptually returns a value.
const ok = window.confirm('Continue?')const ok = await Confirm.call(props)Same await. Your design.
But not only confirmations
Any component you can await.
Each card below is a real Callable. Click any "Try it" to see the actual .call() happen — the badge below the button shows what the promise resolved with.
Menu
Command palette
⌘K-style search. Arrow keys to navigate, Enter to run.
Drawer
Bottom sheet
Slides up from the bottom — resolves with the action you tap.
Flow
Multi-step wizard
A 3-step signup — one await resolves with the whole form.
Notification
Progress toast
A singleton that updates itself via upsert() as work progresses.
How it lives in your app
Declared once. Called from anywhere.
The <Confirm /> mount lives in your React tree — like any other component. The Confirm.call(…) happens in imperative code, from anywhere. One Root, many calls.
Your React tree
You mount it once, anywhere visible. The Root listens for calls and renders the active ones as a stack.
Anywhere in your code
The async logic owns the flow — UI gets pulled in only when the logic asks.
Why not just React state?
One question. One handler.
React state shows the dialog, but it can't hand the answer back to the
code that opened it — so the flow splits across handlers. A call() returns the answer right
where you asked.
// 1) the trigger just flips a flag const onDelete = () => setOpen(true) // 2) ...the real work lives in the dialog, // in a handler far from (1) <Confirm onAccept={() => api.delete(id)} … />
// one handler: ask, await, act
const onDelete = async () => {
if (await Confirm.call()) {
await api.delete(id)
}
} The Stack
Many calls. One Root. No conflict.
Each .call() adds an active call to the stack. The Root renders all of them at once. A nested confirm-inside-a-form is just two calls living together.
Closing one doesn't affect the others. The model is concurrent by default — you don't have to wire it up.
Mutation flow
Stay open on failure. Close on success.
The hook tracks pending for you. Your mutationFn decides — by calling call.end() or not — whether the dialog closes.
Throw, and the call stays open. The user retries without losing their input. That single property handles 90% of "save form" flows for you.
Lifecycle log
Ready to make your components await?
1 KB. No deps. SSR. React Native.
🤖 Building with an AI assistant?