Confirm with optional async
One Callable, two callers. Omit mutationFn and submit().orEnd(true) closes instantly with a fallback response; pass one and the async handler decides when to close — the same Confirm serves both.
Mutation flow
→ awaiting click…
The Callable
declared once, mounted in the React treeimport { createCallable } from 'react-call'import { type MutationFn, useMutationFlow } from 'react-call/mutation-flow'
interface Props { message: string // Optional: type it as possibly-undefined to unlock `.orEnd(value)` at // the callsite. Callers may pass an async handler or skip it entirely. mutationFn?: MutationFn<boolean>}
export const Confirm = createCallable<Props, boolean>( ({ call, message, mutationFn }) => { const submit = useMutationFlow(call, mutationFn)
return ( <div role="dialog" aria-modal="true" className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm" > <div className="w-full max-w-sm rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] p-6 shadow-2xl"> <p className="text-base text-[var(--color-fg)]">{message}</p> <div className="mt-6 flex items-center justify-end gap-3"> <button type="button" disabled={submit.pending} onClick={() => call.end(false)} className="rounded-md border border-[var(--color-border)] px-4 py-2 text-sm font-medium text-[var(--color-fg-muted)] transition-colors hover:text-[var(--color-fg)] disabled:opacity-50" > Cancel </button> <button type="button" disabled={submit.pending} // No mutationFn → `.orEnd(true)` closes with the fallback. // With one → `.orEnd` is a no-op; the handler closes instead. onClick={() => submit().orEnd(true)} className="rounded-md bg-[var(--color-accent)] px-4 py-2 text-sm font-medium text-[var(--color-accent-fg)] transition-colors hover:bg-[var(--color-accent-hover)] disabled:opacity-50" > {submit.pending ? 'Working…' : 'Confirm'} </button> </div> </div> </div> ) },)Confirm.displayName = 'Confirm'The Root
mounted in your app tree, onceimport { Confirm } from './Confirm'import { PublishButton } from './PublishButton'
export default function App() { return ( <> <Confirm /> <PublishButton /> </> )}The caller
anywhere in your app, imperativeimport { useState } from 'react'import { Confirm } from './Confirm'
const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))
export const PublishButton = () => { const [status, setStatus] = useState<string>('→ awaiting click…')
// No mutationFn: `submit().orEnd(true)` closes the call instantly with // the fallback response. A plain confirm — no async, no pending state. const quickConfirm = async () => { const ok = await Confirm.call({ message: 'Discard your draft?' }) setStatus(ok ? '→ draft discarded' : '→ kept') }
// With a mutationFn: `.orEnd` becomes a no-op. The handler owns the // close — pending shows while it runs, then it calls call.end(true). const confirmAndPublish = async () => { const ok = await Confirm.call({ message: 'Publish this post now?', mutationFn: async (call) => { await sleep(900) call.end(true) }, }) setStatus(ok ? '→ published' : '→ cancelled') }
return ( <div className="flex flex-col items-center gap-3"> <div className="flex gap-3"> <button type="button" onClick={quickConfirm} className="rounded-md border border-[var(--color-border)] px-4 py-2 text-sm font-medium text-[var(--color-fg-muted)] transition-colors hover:text-[var(--color-fg)]" > Discard draft </button> <button type="button" onClick={confirmAndPublish} className="rounded-md bg-[var(--color-accent)] px-4 py-2 text-sm font-medium text-[var(--color-accent-fg)] transition-colors hover:bg-[var(--color-accent-hover)]" > Publish post </button> </div> <span className="font-mono text-xs text-[var(--color-fg-subtle)]"> {status} </span> </div> )}One Callable, two close paths
Typing mutationFn? as possibly-undefined changes what the trigger
returns: instead of void, submit() hands back a chain object with
.orEnd(value). That one chain covers both callers:
- No handler (the Discard draft button) →
submit().orEnd(true)delivers the fallback response and closes the call right away. No async, no pending — a plain confirm. - With a handler (the Publish post button) →
.orEndis a no-op. The MutationFn runs (pending flips true), then it callscall.end(true).
The Callable doesn’t branch on which caller it’s serving — useMutationFlow
resolves it from whether mutationFn is present at runtime.
See Save form for the required-handler case, where the dialog stays open on a thrown error so the user can retry.