UNPKG

react-hot-toast

Version:

Smoking hot React Notifications. Lightweight, customizable and beautiful by default.

187 lines (166 loc) 4.05 kB
import { useEffect, useState, useRef } from 'react'; import { DefaultToastOptions, Toast, ToastType } from './types'; const TOAST_LIMIT = 20; export enum ActionType { ADD_TOAST, UPDATE_TOAST, UPSERT_TOAST, DISMISS_TOAST, REMOVE_TOAST, START_PAUSE, END_PAUSE, } type Action = | { type: ActionType.ADD_TOAST; toast: Toast; } | { type: ActionType.UPSERT_TOAST; toast: Toast; } | { type: ActionType.UPDATE_TOAST; toast: Partial<Toast>; } | { type: ActionType.DISMISS_TOAST; toastId?: string; } | { type: ActionType.REMOVE_TOAST; toastId?: string; } | { type: ActionType.START_PAUSE; time: number; } | { type: ActionType.END_PAUSE; time: number; }; interface State { toasts: Toast[]; pausedAt: number | undefined; } export const reducer = (state: State, action: Action): State => { switch (action.type) { case ActionType.ADD_TOAST: return { ...state, toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), }; case ActionType.UPDATE_TOAST: return { ...state, toasts: state.toasts.map((t) => t.id === action.toast.id ? { ...t, ...action.toast } : t ), }; case ActionType.UPSERT_TOAST: const { toast } = action; return reducer(state, { type: state.toasts.find((t) => t.id === toast.id) ? ActionType.UPDATE_TOAST : ActionType.ADD_TOAST, toast, }); case ActionType.DISMISS_TOAST: const { toastId } = action; return { ...state, toasts: state.toasts.map((t) => t.id === toastId || toastId === undefined ? { ...t, dismissed: true, visible: false, } : t ), }; case ActionType.REMOVE_TOAST: if (action.toastId === undefined) { return { ...state, toasts: [], }; } return { ...state, toasts: state.toasts.filter((t) => t.id !== action.toastId), }; case ActionType.START_PAUSE: return { ...state, pausedAt: action.time, }; case ActionType.END_PAUSE: const diff = action.time - (state.pausedAt || 0); return { ...state, pausedAt: undefined, toasts: state.toasts.map((t) => ({ ...t, pauseDuration: t.pauseDuration + diff, })), }; } }; const listeners: Array<(state: State) => void> = []; let memoryState: State = { toasts: [], pausedAt: undefined }; export const dispatch = (action: Action) => { memoryState = reducer(memoryState, action); listeners.forEach((listener) => { listener(memoryState); }); }; export const defaultTimeouts: { [key in ToastType]: number; } = { blank: 4000, error: 4000, success: 2000, loading: Infinity, custom: 4000, }; export const useStore = (toastOptions: DefaultToastOptions = {}): State => { const [state, setState] = useState<State>(memoryState); const initial = useRef(memoryState); // TODO: Switch to useSyncExternalStore when targeting React 18+ useEffect(() => { if (initial.current !== memoryState) { setState(memoryState); } listeners.push(setState); return () => { const index = listeners.indexOf(setState); if (index > -1) { listeners.splice(index, 1); } }; }, []); const mergedToasts = state.toasts.map((t) => ({ ...toastOptions, ...toastOptions[t.type], ...t, removeDelay: t.removeDelay || toastOptions[t.type]?.removeDelay || toastOptions?.removeDelay, duration: t.duration || toastOptions[t.type]?.duration || toastOptions?.duration || defaultTimeouts[t.type], style: { ...toastOptions.style, ...toastOptions[t.type]?.style, ...t.style, }, })); return { ...state, toasts: mergedToasts, }; };