react-call v2

Auto-dismissing error

A transient banner that closes itself via setTimeout. Multiple calls stack — each error gets its own banner.

Stacking
no errors yet

The Callable

declared once, mounted in the React tree
ErrorBanner.tsx
import { useEffect } from 'react'
import { createCallable } from 'react-call'
interface Props {
message: string
durationMs: number
}
const ROW_HEIGHT = 52 // px per stacked banner, including gap
export const ErrorBanner = createCallable<Props, void>(
({ call, message, durationMs }) => {
useEffect(() => {
const t = setTimeout(() => call.end(), durationMs)
return () => clearTimeout(t)
}, [call, durationMs])
// call.index reflects this call's position in the active stack — use it
// to offset each banner downward so concurrent toasts don't overlap.
const top = 24 + call.index * ROW_HEIGHT
return (
<div
role="alert"
aria-live="assertive"
style={{ top }}
className="pointer-events-none fixed left-1/2 z-50 -translate-x-1/2 transition-[top] duration-200"
>
<div className="pointer-events-auto flex w-80 max-w-[calc(100vw-1.5rem)] items-center gap-3 rounded-md border border-red-500/40 bg-red-500/10 px-4 py-2 text-sm text-red-200 shadow-2xl backdrop-blur">
<span aria-hidden="true">⚠</span>
<span className="flex-1 truncate">{message}</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-red-300/80 transition-colors hover:bg-red-500/20 hover:text-red-100"
>
×
</button>
</div>
</div>
)
},
)
ErrorBanner.displayName = 'ErrorBanner'

The Root

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

The caller

anywhere in your app, imperative
TriggerErrorButton.tsx
import { useState } from 'react'
import { ErrorBanner } from './ErrorBanner'
export const TriggerErrorButton = () => {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount((c) => c + 1)
ErrorBanner.call({
message: `Network request failed (#${count + 1})`,
durationMs: 1500,
})
}
return (
<div className="flex flex-col items-center gap-3">
<button
type="button"
onClick={handleClick}
className="rounded-md border border-red-500/50 bg-red-500/10 px-4 py-2 text-sm font-medium text-red-300 transition-colors hover:bg-red-500/20"
>
Simulate error
</button>
<span className="font-mono text-xs text-[var(--color-fg-subtle)]">
{count > 0
? `${count} error${count === 1 ? '' : 's'} triggered`
: 'no errors yet'}
</span>
</div>
)
}

You don’t keep a list of banners

The reflex for “stacking toasts” in React is a useState<Banner[]> — push on trigger, filter on dismiss, map to render. This example keeps no such array. Every ErrorBanner.call(…) is an independent entry in the Root’s Stack, and the Root renders all of them. Trigger ten errors and that’s ten calls; the Stack is the list, and it lives in the library, not in your component.

Because each call is its own Stack entry, it carries its own CallContext — including call.index, this call’s position in the Stack. That’s what spaces the banners apart: the vertical offset is just call.index * ROW_HEIGHT, read inside the Callable with no counter of your own. This is the one example where call.index does real work; most Callables never touch it.

When to reach for it

Use plain .call() whenever concurrent instances should coexist rather than replace each other — error toasts, per-item notifications, anything where a second event shouldn’t clobber the first.

When you instead want a single surface that updates in place — a progress bar, a “saving… → saved” pill — that’s upsert, not stacking. The dividing question is simply: should a new call add a banner, or update the existing one?

Related examples