Resolve from the caller
The promise from call() is the call's identity. A timeout in the caller settles that exact open call from the outside with Approval.end(promise, false) — delivering a response without any in-dialog click.
The Callable
declared once, mounted in the React treeimport { createCallable } from 'react-call'
interface Props { action: string}type Response = boolean
export const Approval = createCallable<Props, Response>(({ call, action }) => ( <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)]">Approve: {action}?</p> <p className="mt-1 text-xs text-[var(--color-fg-subtle)]"> Auto-declines from the caller if you don't respond in time. </p> <div className="mt-6 flex items-center justify-end gap-3"> <button type="button" 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)]" > Decline </button> <button type="button" onClick={() => call.end(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)]" > Approve </button> </div> </div> </div>))Approval.displayName = 'Approval'The Root
mounted in your app tree, onceimport { Approval } from './Approval'import { RequestApprovalButton } from './RequestApprovalButton'
export default function App() { return ( <> <Approval /> <RequestApprovalButton /> </> )}The caller
anywhere in your app, imperativeimport { useState } from 'react'import { Approval } from './Approval'
const TIMEOUT_SECONDS = 4
export const RequestApprovalButton = () => { const [status, setStatus] = useState<string>('→ no request yet') const [secondsLeft, setSecondsLeft] = useState<number | null>(null)
const handleClick = async () => { setStatus('awaiting approval…')
// The promise returned by call() IS the call's identity. Hold it so // the timeout can settle THIS specific open call from out here. const promise = Approval.call({ action: 'Deploy to production' })
let answered = false promise.then(() => { answered = true })
// A plain caller-side countdown — no update() needed, it's local UI. let left = TIMEOUT_SECONDS setSecondsLeft(left) const ticker = setInterval(() => { left -= 1 setSecondsLeft(left > 0 ? left : null) if (left <= 0) clearInterval(ticker) }, 1000)
let timedOut = false const timer = setTimeout(() => { if (answered) return timedOut = true // End the call from caller scope, delivering a Response value. // Targeted at `promise`, so only this call is settled. Approval.end(promise, false) }, TIMEOUT_SECONDS * 1000)
const approved = await promise clearTimeout(timer) clearInterval(ticker) setSecondsLeft(null) setStatus( approved ? '→ approved' : timedOut ? '→ auto-declined (timed out)' : '→ declined', ) }
return ( <div className="flex flex-col items-center gap-3"> <button type="button" onClick={handleClick} disabled={secondsLeft !== null} 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" > Request approval </button> <span className="font-mono text-xs text-[var(--color-fg-subtle)]"> {secondsLeft !== null ? ( <span className="text-[var(--color-accent)]"> auto-declines in {secondsLeft}s… </span> ) : ( status )} </span> </div> )}The promise is the call
Approval.call(...) returns a promise that’s both the awaited response
and a handle to that specific open call. Pass it back to
Approval.end(promise, value) and you settle that call from anywhere —
here, a setTimeout that auto-declines if the user goes quiet:
const promise = Approval.call({ action: 'Deploy to production' })setTimeout(() => Approval.end(promise, false), 4000)const approved = await promise // resolves whoever wins: user or timeoutWhether the user clicks a button (call.end inside) or the timeout fires
(Approval.end outside), the same promise resolves and the dialog closes.
Targeted vs. all
Pass the promise to hit one call. Omit it to end every open call at once, with one response value:
Approval.end(false) // decline all open approvalsUseful for a global “cancel everything” — a route change, a logout, a parent flow that’s been abandoned.
Gotchas
- Ending a settled call is a no-op. If the user answers first, the
later
Approval.end(promise, …)does nothing — no need to cancel the timer for correctness (we clear it anyway to stop the countdown). - This is the update story in reverse: there the caller pushes new props into the open call; here it closes it.