Imperative notifications — toasts
The pattern
Mount <Toaster /> once at the app root, then call toast() from anywhere (event handlers, store actions, effects — no hook-order concerns):
import { toast, Toaster } from '@pyreon/toast'
<App>
<MainContent />
<Toaster />
</App>
// Anywhere in your app:
function SaveButton() {
async function onClick() {
try {
await api.save()
toast.success('Saved')
} catch (err) {
toast.error(`Failed: ${err.message}`)
}
}
return <button onClick={onClick}>Save</button>
}Presets: toast.success, toast.error, toast.warning, toast.info, toast.loading. Each returns an ID so you can update the toast later:
const id = toast.loading('Saving…')
try {
await api.save()
toast.update(id, { type: 'success', message: 'Saved' })
} catch (err) {
toast.update(id, { type: 'error', message: `Failed: ${err.message}` })
}Or use toast.promise(...) which handles the state machine for you:
toast.promise(api.save(), {
loading: 'Saving…',
success: 'Saved',
error: (err) => `Failed: ${err.message}`,
})Why
Toasts are inherently imperative — they're triggered by an event (a save succeeding, a button click, a WebSocket message) rather than driven by reactive state. The callable API toast(message) mirrors that shape directly. <Toaster /> handles the rendering, cleanup, pause-on-hover, and a11y (role="alert", aria-live="polite").
Mount the Toaster ONCE at app root. Multiple Toaster instances stack visually and confuse focus management.
Anti-pattern
// BROKEN — constructing a toast signal and mounting manually
function BadRoot() {
const toastMessage = signal('')
return <div class="toast">{() => toastMessage()}</div>
}
// This is the whole reason @pyreon/toast exists. Use it instead.// BROKEN — <Toaster /> inside a conditional mount
function BadRoot2() {
return <>{() => showToaster() && <Toaster />}</>
}
// Toaster is singleton-ish; toggling it dismisses active toasts.
// Mount it once at the root, always on.// BROKEN — calling toast() inside a render body fires on every render
function BadComponent() {
toast('rendered!') // fires once the FIRST render, then again if anything
// upstream triggers re-creation
return <div>...</div>
}
// Correct — gate on an event:
function GoodComponent() {
return <button onClick={() => toast('clicked!')}>Click</button>
}Related
Reference API:
toast,Toaster,toast.promise—get_api({ package: "toast", symbol: "..." })For component-local feedback (input errors, validation), prefer
useField().error()— toast is for cross-cutting events