pyreon

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>
}
  • Reference API: toast, Toaster, toast.promiseget_api({ package: "toast", symbol: "..." })

  • For component-local feedback (input errors, validation), prefer useField().error() — toast is for cross-cutting events

Imperative notifications — toasts