react-call v2

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.

End from caller
→ no request yet

The Callable

declared once, mounted in the React tree
Approval.tsx
import { 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, once
App.tsx
import { Approval } from './Approval'
import { RequestApprovalButton } from './RequestApprovalButton'
export default function App() {
return (
<>
<Approval />
<RequestApprovalButton />
</>
)
}

The caller

anywhere in your app, imperative
RequestApprovalButton.tsx
import { 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 timeout

Whether 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 approvals

Useful 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.

Related examples