import * as React from "react" const TOAST_LIMIT = 1 const TOAST_REMOVE_DELAY = 1000000 type ToasterToast = { id: string title?: React.ReactNode description?: React.ReactNode action?: React.ReactElement open?: boolean onOpenChange?: (open: boolean) => void variant?: "default" | "destructive" } const actionTypes = { ADD_TOAST: "ADD_TOAST", UPDATE_TOAST: "UPDATE_TOAST", DISMISS_TOAST: "DISMISS_TOAST", REMOVE_TOAST: "REMOVE_TOAST", } as const let count = 0 function genId() { count = (count + 1) % Number.MAX_SAFE_INTEGER return count.toString() } type Action = | { type: typeof actionTypes.ADD_TOAST; toast: ToasterToast } | { type: typeof actionTypes.UPDATE_TOAST; toast: Partial } | { type: typeof actionTypes.DISMISS_TOAST; toastId?: string } | { type: typeof actionTypes.REMOVE_TOAST; toastId?: string } interface State { toasts: ToasterToast[] } const toastTimeouts = new Map>() const addToRemoveQueue = (toastId: string) => { if (toastTimeouts.has(toastId)) return const timeout = setTimeout(() => { toastTimeouts.delete(toastId) dispatch({ type: actionTypes.REMOVE_TOAST, toastId }) }, TOAST_REMOVE_DELAY) toastTimeouts.set(toastId, timeout) } 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 if (toastId) addToRemoveQueue(toastId) else state.toasts.forEach((t) => addToRemoveQueue(t.id)) return { ...state, toasts: state.toasts.map((t) => (toastId == null || t.id === toastId ? { ...t, open: false } : t)) } } case actionTypes.REMOVE_TOAST: return { ...state, toasts: action.toastId == null ? [] : state.toasts.filter((t) => t.id !== action.toastId) } } } const listeners: Array<(state: State) => void> = [] let memoryState: State = { toasts: [] } function dispatch(action: Action) { memoryState = reducer(memoryState, action) listeners.forEach((listener) => listener(memoryState)) } type Toast = Omit function 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, dismiss, update } } function useToast() { const [state, setState] = React.useState(memoryState) React.useEffect(() => { listeners.push(setState) return () => { const i = listeners.indexOf(setState); if (i > -1) listeners.splice(i, 1) } }, [state]) return { ...state, toast, dismiss: (toastId?: string) => dispatch({ type: actionTypes.DISMISS_TOAST, toastId }) } } export { useToast, toast }