An opinionated toast component for React providing comprehensive toast notifications with customization options
—
React hooks for integrating with toast state and managing toast notifications programmatically within React components.
React hook for accessing the current toast state, useful for building custom toast-aware components or debugging.
/**
* React hook that provides access to current toast state
* @returns Object containing array of active toasts
*/
function useSonner(): {
toasts: ToastT[];
};
interface ToastT {
id: number | string;
title?: (() => React.ReactNode) | React.ReactNode;
type?: "normal" | "action" | "success" | "info" | "warning" | "error" | "loading" | "default";
icon?: React.ReactNode;
jsx?: React.ReactNode;
richColors?: boolean;
invert?: boolean;
closeButton?: boolean;
dismissible?: boolean;
description?: (() => React.ReactNode) | React.ReactNode;
duration?: number;
delete?: boolean;
action?: Action | React.ReactNode;
cancel?: Action | React.ReactNode;
onDismiss?: (toast: ToastT) => void;
onAutoClose?: (toast: ToastT) => void;
promise?: Promise<any> | (() => Promise<any>);
cancelButtonStyle?: React.CSSProperties;
actionButtonStyle?: React.CSSProperties;
style?: React.CSSProperties;
unstyled?: boolean;
className?: string;
classNames?: ToastClassnames;
descriptionClassName?: string;
position?: Position;
}import React from "react";
import { useSonner, toast } from "sonner";
function ToastStatus() {
const { toasts } = useSonner();
return (
<div>
<p>Active toasts: {toasts.length}</p>
<button onClick={() => toast("New toast!")}>
Add Toast
</button>
<button onClick={() => toast.dismiss()}>
Clear All
</button>
</div>
);
}import React from "react";
import { useSonner, toast } from "sonner";
function ToastAnalytics() {
const { toasts } = useSonner();
const toastCounts = React.useMemo(() => {
return toasts.reduce((counts, toast) => {
const type = toast.type || "default";
counts[type] = (counts[type] || 0) + 1;
return counts;
}, {});
}, [toasts]);
return (
<div className="toast-analytics">
<h3>Current Toast Status</h3>
{Object.entries(toastCounts).map(([type, count]) => (
<div key={type}>
{type}: {count}
</div>
))}
<div className="controls">
<button onClick={() => toast.success("Success!")}>
Add Success
</button>
<button onClick={() => toast.error("Error!")}>
Add Error
</button>
<button onClick={() => toast.loading("Loading...")}>
Add Loading
</button>
</div>
</div>
);
}import React from "react";
import { useSonner } from "sonner";
function LoadingIndicator() {
const { toasts } = useSonner();
const hasLoadingToasts = toasts.some(toast => toast.type === "loading");
const loadingCount = toasts.filter(toast => toast.type === "loading").length;
if (!hasLoadingToasts) {
return null;
}
return (
<div className="loading-indicator">
<div className="spinner" />
<span>
{loadingCount === 1
? "Processing..."
: `${loadingCount} operations in progress...`
}
</span>
</div>
);
}import React from "react";
import { useSonner, toast } from "sonner";
function ToastManager() {
const { toasts } = useSonner();
const dismissToast = (id: string | number) => {
toast.dismiss(id);
};
const dismissByType = (type: string) => {
toasts
.filter(t => t.type === type)
.forEach(t => toast.dismiss(t.id));
};
const dismissOlderThan = (minutes: number) => {
const cutoff = Date.now() - (minutes * 60 * 1000);
toasts
.filter(t => {
// Approximate age based on ID if it's a timestamp
const toastTime = typeof t.id === 'number' ? t.id : Date.now();
return toastTime < cutoff;
})
.forEach(t => toast.dismiss(t.id));
};
return (
<div className="toast-manager">
<h3>Toast Manager ({toasts.length} active)</h3>
<div className="toast-list">
{toasts.map(toast => (
<div key={toast.id} className="toast-item">
<span className={`toast-type ${toast.type}`}>
{toast.type || "default"}
</span>
<span className="toast-title">
{typeof toast.title === "function" ? toast.title() : toast.title}
</span>
<button onClick={() => dismissToast(toast.id)}>
×
</button>
</div>
))}
</div>
<div className="controls">
<button onClick={() => dismissByType("error")}>
Clear Errors
</button>
<button onClick={() => dismissByType("success")}>
Clear Success
</button>
<button onClick={() => dismissOlderThan(5)}>
Clear Old (5+ min)
</button>
<button onClick={() => toast.dismiss()}>
Clear All
</button>
</div>
</div>
);
}import React from "react";
import { useSonner } from "sonner";
function ToastPersistenceMonitor() {
const { toasts } = useSonner();
const [persistentToasts, setPersistentToasts] = React.useState([]);
React.useEffect(() => {
// Track toasts that have been around for a long time
const persistent = toasts.filter(toast => {
// Check if duration is Infinity or very long
return toast.duration === Infinity ||
(toast.duration && toast.duration > 30000) ||
toast.type === "loading";
});
setPersistentToasts(persistent);
}, [toasts]);
if (persistentToasts.length === 0) {
return null;
}
return (
<div className="persistence-monitor">
<h4>⚠️ Persistent Toasts ({persistentToasts.length})</h4>
<p>These toasts won't auto-dismiss:</p>
<ul>
{persistentToasts.map(toast => (
<li key={toast.id}>
{toast.type}: {typeof toast.title === "function" ? toast.title() : toast.title}
</li>
))}
</ul>
</div>
);
}import React from "react";
import { useSonner } from "sonner";
function ToastPerformanceMonitor() {
const { toasts } = useSonner();
const [maxToasts, setMaxToasts] = React.useState(0);
const [toastHistory, setToastHistory] = React.useState([]);
React.useEffect(() => {
setMaxToasts(Math.max(maxToasts, toasts.length));
// Keep a history of toast count changes
setToastHistory(prev => [
...prev.slice(-20), // Keep last 20 entries
{ timestamp: Date.now(), count: toasts.length }
]);
}, [toasts.length]);
const averageToasts = React.useMemo(() => {
if (toastHistory.length === 0) return 0;
const sum = toastHistory.reduce((acc, entry) => acc + entry.count, 0);
return (sum / toastHistory.length).toFixed(1);
}, [toastHistory]);
return (
<div className="performance-monitor">
<h4>Toast Performance</h4>
<div className="metrics">
<div>Current: {toasts.length}</div>
<div>Peak: {maxToasts}</div>
<div>Average: {averageToasts}</div>
</div>
{toasts.length > 5 && (
<div className="warning">
⚠️ High toast count may impact performance
</div>
)}
</div>
);
}import React from "react";
import { useSonner } from "sonner";
function AppWithToastAwareness() {
const { toasts } = useSonner();
const hasErrors = toasts.some(t => t.type === "error");
const hasLoading = toasts.some(t => t.type === "loading");
const hasSuccessToasts = toasts.some(t => t.type === "success");
return (
<div className={`app ${hasErrors ? "has-errors" : ""}`}>
<header>
<h1>My App</h1>
{hasLoading && <div className="loading-indicator">Processing...</div>}
</header>
<main>
{hasErrors && (
<div className="error-state">
<p>Some operations failed. Check notifications for details.</p>
</div>
)}
{hasSuccessToasts && (
<div className="success-state">
<p>✅ Recent successful operations</p>
</div>
)}
<YourAppContent />
</main>
{toasts.length > 0 && (
<div className="toast-summary">
{toasts.length} active notification{toasts.length !== 1 ? "s" : ""}
</div>
)}
</div>
);
}import React from "react";
import { useSonner, toast } from "sonner";
const ToastContext = React.createContext({
toasts: [],
addToast: (message, options) => {},
clearToasts: () => {},
hasToasts: false,
});
export function ToastProvider({ children }) {
const { toasts } = useSonner();
const contextValue = React.useMemo(() => ({
toasts,
addToast: (message, options) => toast(message, options),
clearToasts: () => toast.dismiss(),
hasToasts: toasts.length > 0,
}), [toasts]);
return (
<ToastContext.Provider value={contextValue}>
{children}
</ToastContext.Provider>
);
}
export function useToastContext() {
return React.useContext(ToastContext);
}import React from "react";
import { useSonner } from "sonner";
function useToastSync() {
const { toasts } = useSonner();
// Sync toast state to localStorage
React.useEffect(() => {
const toastData = toasts.map(t => ({
id: t.id,
type: t.type,
title: typeof t.title === "string" ? t.title : "[React Element]",
timestamp: Date.now()
}));
localStorage.setItem("recent-toasts", JSON.stringify(toastData));
}, [toasts]);
// Analytics tracking
React.useEffect(() => {
if (typeof window !== "undefined" && window.gtag) {
toasts.forEach(toast => {
if (toast.type === "error") {
window.gtag("event", "toast_error", {
error_type: toast.type,
error_message: toast.title
});
}
});
}
}, [toasts]);
}The useSonner hook provides real-time access to the current toast state, enabling these advanced patterns while maintaining performance through React's built-in optimization mechanisms.
Install with Tessl CLI
npx tessl i tessl/npm-sonner