Files
skills/docs/ui/react/react-ui/src/components/use-toast-sonner-bridge/use-toast-sonner-bridge.md
2026-05-29 08:20:56 +01:00

4.1 KiB

useToastSonnerBridge

(no submodules)

Purpose

Bridge hook that exposes the legacy chassis useToast API on top of sonner.toast. The original chassis useToast (Wave 2) ran a small useSyncExternalStore-backed queue that a separate root-level renderer painted; Wave 3g adopts sonner as the canonical toast surface (per shadcn skill rule) so the chassis-side queue is no longer the canonical path. This bridge lets consumers migrate one import at a time — call sites that already imported the legacy useToast hook keep working, and new call sites that want the sonner-native surface adopt this bridge instead.

The two routes funnel through the same react_ui.toast.shown_total{toast.kind=...} counter so observability dashboards never double-count.

Features

  • bridge.toast({ titleKey, descriptionKey?, kind? }) — branded translation keys, resolved against the active <TranslationProvider>, dispatched to sonner.
  • bridge.dismiss(id?) — dismiss a specific toast id, or every active toast when called without arguments.
  • Kind-aware dispatch: success, error, info, warning route to the matching toast.<kind> method; everything else is dispatched via the default toast() callable.
  • Zero internal state — the bridge is a stateless adapter; the authoritative queue lives inside sonner.

Usage: Basic Example

import {
    Sonner,
    useToastSonnerBridge,
} from "@suaveplan/react-ui";

function SaveButton(): JSX.Element {
    const { toast } = useToastSonnerBridge();
    return (
        <>
            <button
                type="button"
                onClick={() =>
                    toast({
                        titleKey: "react-ui.loading",
                        kind: "success",
                    })
                }
            >
                Save
            </button>
            <Sonner />
        </>
    );
}

Usage: Advanced Example

import {
    Sonner,
    useToastSonnerBridge,
} from "@suaveplan/react-ui";
import { useEffect } from "react";

function NetworkBoundary(): JSX.Element {
    const { toast, dismiss } = useToastSonnerBridge();

    useEffect(() => {
        const id = toast({
            titleKey: "react-ui.loading",
            descriptionKey: "react-ui.empty.noResults",
            kind: "warning",
        });
        const timer = setTimeout(() => dismiss(id), 4000);
        return () => clearTimeout(timer);
    }, [toast, dismiss]);

    return <Sonner position="top-right" />;
}

API Reference

useToastSonnerBridge(): UseToastSonnerBridgeReturn

Returns:

  • toast(input: BridgeToastInput): string | number — emits a toast via sonner. The returned id is the same id sonner assigns; pass it to dismiss to retire the specific toast.
  • dismiss(id?: string | number): void — dismisses one toast by id, or all toasts when id is undefined.

BridgeToastInput

  • titleKey: TranslationKey — REQUIRED branded key resolved against the active i18n catalog.
  • descriptionKey?: TranslationKey — optional secondary line, also resolved against i18n.
  • kind?: ToastKind — 'default' | 'success' | 'error' | 'info' | 'warning'. Defaults to 'default'.

Implementation Notes

  • The bridge MUST be called inside a <TranslationProvider> because it reads useTranslation() to resolve the branded keys; outside a provider it throws REACT_I18N.NO_PROVIDER.
  • The bridge does NOT instrument OTel directly — the underlying toast re-export (which records the counter) is the single point of attribution so we never double-count when the bridge fans out to a kind-specific method.
  • The bridge does NOT read sonner's useSonner() hook because the upstream API does not expose a way to subscribe to a SPECIFIC toast id, and the chassis bridge has no use for the global toast list at call time — toast.dismiss(id) is sufficient for the UX surface.
  • Memoised useCallback wrappers keep the returned object referentially stable across renders so consumers using the bridge inside a useEffect dependency array do not re-fire on every parent render.