react-call v2
$npm install react-call

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.

The browser
const ok = window.confirm('Continue?')
→ awaiting click…
Your component
const ok = await Confirm.call(props)
→ awaiting click…

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.

See code ↗
→ awaiting click…

Drawer

Bottom sheet

Slides up from the bottom — resolves with the action you tap.

See code ↗
→ awaiting click…

Flow

Multi-step wizard

A 3-step signup — one await resolves with the whole form.

See code ↗
→ awaiting click…

Picker

Color picker

Click a swatch — resolves with the hex value.

See code ↗
→ awaiting click…

Menu

Context menu

Forwards the cursor position to a positioned Callable.

See code ↗
→ awaiting click…

Notification

Progress toast

A singleton that updates itself via upsert() as work progresses.

See code ↗
→ awaiting click…

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

<App>
<Header />
<Routes />
<Confirm />← the Root
</App>

You mount it once, anywhere visible. The Root listens for calls and renders the active ones as a stack.

.call()
Render
Response

Anywhere in your code

// inside any handler, hook, or domain action
const accepted = await Confirm.call()
if (accepted) await api.delete(id)

The async logic owns the flow — UI gets pulled in only when the logic asks.

idlecallingrenderingresolving

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.

React state
// 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)} … />
With react-call
// 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.

0 / 5 active
click "Open another" to start a call

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.

See code ↗
the Save dialog will appear here

Lifecycle log

open the dialog and click Save…

Ready to make your components await?

1 KB. No deps. SSR. React Native.

$npm install react-call

🤖 Building with an AI assistant?

$npx skills add desko27/react-call --skill react-call
What the skill does ↗