react-call v2

Broadcast to every call

Several upload pills stacked at once. One Upload.update(props) with no promise merges into every open call, so a single connection change flips them all — while each keeps its own filename.

Update Stacking
→ start some uploads, then broadcast to all of them

The Callable

declared once, mounted in the React tree
Upload.tsx
import { createCallable } from 'react-call'
type UploadState = 'uploading' | 'paused' | 'done'
interface Props {
label: string
state: UploadState
}
const ROW_HEIGHT = 52 // px per stacked pill, including gap
const STATE_META: Record<UploadState, { icon: string; text: string }> = {
uploading: { icon: '↑', text: 'uploading…' },
paused: { icon: '⏸', text: 'paused' },
done: { icon: '✓', text: 'done' },
}
export const Upload = createCallable<Props, void>(({ call, label, state }) => {
// call.index is this call's slot in the stack — offset each pill upward
// so concurrent uploads stack instead of overlapping.
const bottom = 24 + call.index * ROW_HEIGHT
const { icon, text } = STATE_META[state]
return (
<div
style={{ bottom }}
className="pointer-events-none fixed right-6 z-50 transition-[bottom] duration-200"
>
<div className="pointer-events-auto flex w-72 items-center gap-3 rounded-md border border-[var(--color-border)] bg-[var(--color-bg)] px-4 py-2 text-sm text-[var(--color-fg)] shadow-2xl backdrop-blur">
<span aria-hidden="true">{icon}</span>
<span className="flex-1 truncate">{label}</span>
<span className="font-mono text-xs text-[var(--color-fg-subtle)]">
{text}
</span>
<button
type="button"
onClick={() => call.end()}
aria-label="Dismiss"
className="-mr-1 inline-flex h-7 w-7 shrink-0 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>
)
})
Upload.displayName = 'Upload'

The Root

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

The caller

anywhere in your app, imperative
UploadQueueButton.tsx
import { useState } from 'react'
import { Upload } from './Upload'
const FILES = ['report.pdf', 'photo.jpg', 'archive.zip']
export const UploadQueueButton = () => {
const [online, setOnline] = useState(true)
const [openCount, setOpenCount] = useState(0)
const start = () => {
setOnline(true)
for (const label of FILES) {
// The promise resolves when this pill is dismissed (× → call.end()),
// so we can track how many are still open and re-enable Start at zero.
const promise = Upload.call({ label, state: 'uploading' })
setOpenCount((n) => n + 1)
promise.then(() => setOpenCount((n) => n - 1))
}
}
// One update() with no promise broadcasts to EVERY open call. It merges
// `state` into each pill's props, so they all flip together while each
// keeps its own `label`.
const toggleConnection = () => {
const next = !online
setOnline(next)
Upload.update({ state: next ? 'uploading' : 'paused' })
}
const completeAll = () => {
setOnline(true)
Upload.update({ state: 'done' })
}
const idle = openCount === 0
return (
<div className="flex flex-col items-center gap-3">
<div className="flex flex-wrap justify-center gap-3">
<button
type="button"
onClick={start}
disabled={!idle}
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"
>
Start 3 uploads
</button>
<button
type="button"
onClick={toggleConnection}
disabled={idle}
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"
>
Toggle connection
</button>
<button
type="button"
onClick={completeAll}
disabled={idle}
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"
>
Complete all
</button>
</div>
<span className="font-mono text-xs text-[var(--color-fg-subtle)]">
{idle
? '→ start some uploads, then broadcast to all of them'
: `connection: ${online ? 'online' : 'offline'} — ${openCount} open`}
</span>
</div>
)
}

Update without a promise hits everyone

update has two forms. Pass a promise to target one call; omit it and the new props broadcast to every open call:

Upload.update({ state: 'paused' }) // every open pill, at once

It’s a shallow merge, not a replace — state lands on each call while its own label stays put. That’s why one “connection lost” event can pause three uploads without the caller tracking a single promise.

Broadcast vs. targeted

This is the counterpart to Live status:

  • Targetedupdate(promise, props) pushes to one specific call the caller is tracking. Use it when each call has its own lifecycle.
  • Broadcastupdate(props) pushes to all of them. Use it for ambient state every open call should reflect: connectivity, a global mode, a shared clock.

Gotchas

  • Merging means a partial broadcast leaves untouched props alone — handy, but double-check you’re not relying on a prop a stale call still holds.

Related examples