react-call v2

Live status update

A pinned status pill. The caller pushes new props into the open call as work advances — same instance, updated from the outside via the promise reference.

Update

The Callable

declared once, mounted in the React tree
Status.tsx
import { createCallable } from 'react-call'
type Stage = 'placed' | 'packing' | 'shipped' | 'out' | 'delivered'
const STAGES: Stage[] = ['placed', 'packing', 'shipped', 'out', 'delivered']
const LABELS: Record<Stage, string> = {
placed: 'Order placed',
packing: 'Packing your order',
shipped: 'Handed to carrier',
out: 'Out for delivery',
delivered: 'Delivered',
}
interface Props {
stage: Stage
}
export const Status = createCallable<Props, void>(({ call, stage }) => {
const stepIndex = STAGES.indexOf(stage)
const done = stage === 'delivered'
return (
<div className="pointer-events-none fixed right-6 bottom-6 z-50">
<div className="pointer-events-auto w-[280px] rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] p-4 shadow-2xl">
<div className="flex items-start justify-between gap-3">
<div>
<p className="font-mono text-[10px] uppercase tracking-wider text-[var(--color-fg-subtle)]">
Order #4821
</p>
<p className="mt-0.5 text-sm font-medium text-[var(--color-fg)]">
{LABELS[stage]}
</p>
</div>
<button
type="button"
onClick={() => call.end()}
aria-label={done ? 'Dismiss' : 'Stop watching'}
className="-mr-1 inline-flex h-7 w-7 items-center justify-center rounded-md text-base leading-none text-[var(--color-fg-subtle)] transition-colors hover:bg-[var(--color-bg-subtle)] hover:text-[var(--color-fg)]"
>
×
</button>
</div>
<div className="mt-3 flex gap-1">
{STAGES.map((s, i) => (
<div
key={s}
className={
i <= stepIndex
? 'h-1 flex-1 rounded-full bg-[var(--color-accent)] transition-colors'
: 'h-1 flex-1 rounded-full bg-[var(--color-bg-muted)] transition-colors'
}
/>
))}
</div>
</div>
</div>
)
})
Status.displayName = 'Status'

The Root

mounted in your app tree, once
App.tsx
import { Status } from './Status'
import { PlaceOrderButton } from './PlaceOrderButton'
export default function App() {
return (
<>
<Status />
<PlaceOrderButton />
</>
)
}

The caller

anywhere in your app, imperative
PlaceOrderButton.tsx
import { useState } from 'react'
import { Status } from './Status'
type Stage = 'placed' | 'packing' | 'shipped' | 'out' | 'delivered'
const SEQUENCE: Stage[] = ['packing', 'shipped', 'out', 'delivered']
const sleep = (ms: number) =>
new Promise<void>((resolve) => setTimeout(resolve, ms))
export const PlaceOrderButton = () => {
const [running, setRunning] = useState(false)
const handleClick = async () => {
setRunning(true)
// The promise IS the call identity — hold the reference so we can
// push updates to *this* specific open call later on.
const promise = Status.call({ stage: 'placed' })
// Stop pushing if the user dismisses early.
let dismissed = false
promise.then(() => {
dismissed = true
})
for (const stage of SEQUENCE) {
await sleep(900)
if (dismissed) break
// Re-render the open Status with new props — no new instance,
// no upsert ambiguity, just this call.
Status.update(promise, { stage })
}
await promise
setRunning(false)
}
return (
<button
type="button"
disabled={running}
onClick={handleClick}
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"
>
{running ? 'Watching order…' : 'Place order'}
</button>
)
}

Why update and not upsert

call() returns a promise that doubles as the call identity. Pass that same promise back to Callable.update(promise, newProps) and the same open call re-renders with the new props.

That’s the differentiator from upsert: with update, you choose exactly which open call to push to — and updates only land if it’s still open. (Without the promise, the only fallback is Callable.update(props), which broadcasts to every open instance.) Multiple parallel orders, each tracked by its own promise, no fighting for a singleton slot.

When to reach for it

  • Long-running operations that emit progress from outside the Callable (websocket frames, server-sent events, timers).
  • A live “viewers count” or “presence” indicator on an already-open dialog.
  • Anywhere the call’s props need to change while it stays mounted, and the caller knows which specific call to target.

Gotchas

  • Updates to a closed call are no-ops. If the user dismisses early, subsequent update(promise, …) calls are silently dropped. Track dismissal via promise.then(…) if your loop needs to stop pushing.

Related examples