Skip to main content
Glama
useToast.tsx10.7 kB
'use client'; import { type ReactNode, useEffect, useState } from 'react'; import type { ToastActionElement, ToastProps } from './Toast'; /** * Maximum number of toasts that can be displayed simultaneously. * Prevents UI overflow and maintains clean notification experience. */ const TOAST_LIMIT = 1; /** * Delay before automatically removing dismissed toasts from memory. * Set to 15 minutes (900,000ms) to allow for potential undo actions. */ const TOAST_REMOVE_DELAY = 15 * 60 * 1000; // 15 seconds /** * Extended toast configuration with additional properties for the toast system. * * Combines base ToastProps with specific fields needed for toast management * including unique identification and content elements. * * @example * ```tsx * const toast: ToasterToast = { * id: '1', * variant: 'success', * title: 'Success!', * description: 'Your action completed successfully.', * action: <ToastAction altText="View details">View</ToastAction> * }; * ``` */ type ToasterToast = ToastProps & { /** Unique identifier for the toast instance */ id: string; /** Optional title text or React element */ title?: ReactNode; /** Optional description text or React element */ description?: ReactNode; /** Optional action button element */ action?: ToastActionElement; }; /** * Action types for toast state management using reducer pattern. * * Defines all possible actions that can be performed on the toast state, * enabling predictable state updates and better debugging. */ enum ActionTypes { /** Add a new toast to the display queue */ ADD_TOAST = 'ADD_TOAST', /** Update properties of an existing toast */ UPDATE_TOAST = 'UPDATE_TOAST', /** Mark a toast as dismissed (triggers exit animation) */ DISMISS_TOAST = 'DISMISS_TOAST', /** Completely remove a toast from memory */ REMOVE_TOAST = 'REMOVE_TOAST', } let count = 0; const genId = () => { count = (count + 1) % Number.MAX_SAFE_INTEGER; return count.toString(); }; type Action = | { type: ActionTypes.ADD_TOAST; toast: ToasterToast; } | { type: ActionTypes.UPDATE_TOAST; toast: Partial<ToasterToast>; } | { type: ActionTypes.DISMISS_TOAST; toastId?: ToasterToast['id']; } | { type: ActionTypes.REMOVE_TOAST; toastId?: ToasterToast['id']; }; type State = { toasts: ToasterToast[]; }; const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>(); const addToRemoveQueue = (toastId: string) => { if (toastTimeouts.has(toastId)) { return; } const timeout = setTimeout(() => { toastTimeouts.delete(toastId); dispatch({ type: ActionTypes.REMOVE_TOAST, toastId: toastId, }); }, TOAST_REMOVE_DELAY); toastTimeouts.set(toastId, timeout); }; /** * Toast state reducer function that handles all toast state transitions. * * Implements predictable state updates using the reducer pattern, ensuring * consistent behavior across all toast operations. * * ## State Management * - **ADD_TOAST**: Adds new toast and enforces limit by removing excess toasts * - **UPDATE_TOAST**: Updates existing toast properties while preserving identity * - **DISMISS_TOAST**: Marks toasts as closed and schedules removal * - **REMOVE_TOAST**: Permanently removes toasts from state * * ## Side Effects * The DISMISS_TOAST action includes side effects for scheduling toast removal, * which could be extracted but is kept here for simplicity. * * @param state - Current toast state * @param action - Action to perform on the state * @returns Updated toast state * * @example * ```tsx * // Add a new toast * const newState = reducer(state, { * type: ActionTypes.ADD_TOAST, * toast: { id: '1', title: 'Hello', variant: 'default' } * }); * * // Dismiss a specific toast * const dismissedState = reducer(state, { * type: ActionTypes.DISMISS_TOAST, * toastId: '1' * }); * ``` */ export const reducer = (state: State, action: Action): State => { switch (action.type) { case ActionTypes.ADD_TOAST: return { ...state, toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), }; case ActionTypes.UPDATE_TOAST: return { ...state, toasts: state.toasts.map((t) => t.id === action.toast.id ? { ...t, ...action.toast } : t ), }; case ActionTypes.DISMISS_TOAST: { const { toastId } = action; // ! Side effects ! - This could be extracted into a dismissToast() action, // but I'll keep it here for simplicity if (toastId) { addToRemoveQueue(toastId); } else { state.toasts.forEach((toast) => { addToRemoveQueue(toast.id); }); } return { ...state, toasts: state.toasts.map((t) => t.id === toastId || toastId === undefined ? { ...t, open: false, } : t ), }; } case ActionTypes.REMOVE_TOAST: if (action.toastId === undefined) { return { ...state, toasts: [], }; } return { ...state, toasts: state.toasts.filter((t) => t.id !== action.toastId), }; } }; const listeners: ((state: State) => void)[] = []; let memoryState: State = { toasts: [] }; const dispatch = (action: Action) => { memoryState = reducer(memoryState, action); listeners.forEach((listener) => { listener(memoryState); }); }; /** * Toast configuration type for creating new toasts. * Omits the 'id' field as it's automatically generated. */ type Toast = Omit<ToasterToast, 'id'>; /** * Creates and displays a new toast notification. * * This is the primary function for showing toast notifications to users. * It automatically generates unique IDs, handles state updates, and provides * control functions for managing the toast lifecycle. * * ## Features * - **Automatic ID Generation**: Each toast gets a unique identifier * - **State Management**: Integrates with global toast state * - **Lifecycle Control**: Returns functions to update or dismiss the toast * - **Auto-dismiss**: Automatically closes when user dismisses * * ## Return Value * Returns an object with control functions: * - `id`: Unique identifier for the toast * - `dismiss()`: Function to manually dismiss the toast * - `update()`: Function to update toast properties * * @param props - Toast configuration (title, description, variant, etc.) * @returns Object with toast ID and control functions * * @example * ```tsx * // Basic success toast * const { dismiss } = toast({ * title: 'Success!', * description: 'Your file was uploaded successfully.', * variant: 'success' * }); * * // Error toast with retry action * const errorToast = toast({ * title: 'Upload Failed', * description: 'Could not upload file. Please try again.', * variant: 'error', * action: <ToastAction altText="Retry upload">Retry</ToastAction> * }); * * // Update toast content dynamically * errorToast.update({ * title: 'Retrying...', * description: 'Please wait while we retry the upload.', * variant: 'default' * }); * * // Manually dismiss toast * setTimeout(() => errorToast.dismiss(), 5000); * ``` */ const toast = ({ ...props }: Toast) => { const id = genId(); const update = (props: ToasterToast) => dispatch({ type: ActionTypes.UPDATE_TOAST, toast: { ...props, id }, }); const dismiss = () => dispatch({ type: ActionTypes.DISMISS_TOAST, toastId: id }); dispatch({ type: ActionTypes.ADD_TOAST, toast: { ...props, id, open: true, onOpenChange: (open) => { if (!open) dismiss(); }, }, }); return { id: id, dismiss, update, }; }; /** * React hook for managing toast notifications. * * Provides access to the global toast state and functions for creating and * managing toast notifications. This hook connects components to the toast * system and ensures reactive updates when toasts change. * * ## Features * - **State Synchronization**: Automatically updates when toast state changes * - **Toast Creation**: Provides the `toast()` function for creating notifications * - **Batch Dismissal**: Can dismiss all toasts or specific toasts by ID * - **Memory Management**: Handles proper cleanup of listeners * * ## Return Value * - `toasts`: Array of current toast objects * - `toast()`: Function to create new toast notifications * - `dismiss()`: Function to dismiss toasts (all or by ID) * * @returns Toast state and control functions * * @example * ```tsx * function NotificationButton() { * const { toast, toasts, dismiss } = useToast(); * * const showSuccess = () => { * toast({ * title: 'Success!', * description: 'Operation completed successfully.', * variant: 'success' * }); * }; * * const clearAll = () => { * dismiss(); // Dismisses all toasts * }; * * return ( * <div> * <button onClick={showSuccess}>Show Success</button> * <button onClick={clearAll}>Clear All ({toasts.length})</button> * </div> * ); * } * * // Usage in a form component * function ContactForm() { * const { toast } = useToast(); * * const handleSubmit = async (data) => { * try { * await submitForm(data); * toast({ * title: 'Form Submitted', * description: 'We\'ll get back to you soon!', * variant: 'success' * }); * } catch (error) { * toast({ * title: 'Submission Failed', * description: 'Please check your connection and try again.', * variant: 'error', * action: <ToastAction altText="Retry">Retry</ToastAction> * }); * } * }; * * // ... form JSX * } * ``` * * ## Usage Notes * - Must be used within a component tree that includes `<Toaster />` * - Toast state is global - changes affect all components using this hook * - Toasts are automatically cleaned up after the configured delay * - Consider UX best practices: don't overwhelm users with too many toasts */ const useToast = () => { const [state, setState] = useState<State>(memoryState); useEffect(() => { listeners.push(setState); return () => { const index = listeners.indexOf(setState); if (index > -1) { listeners.splice(index, 1); } }; }, [state]); return { ...state, toast, dismiss: (toastId?: string) => dispatch({ type: ActionTypes.DISMISS_TOAST, toastId }), }; }; export { toast, useToast };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aymericzip/intlayer'

If you have feedback or need assistance with the MCP directory API, please join our Discord server