"use client"; import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode, } from "react"; type ToastKind = "info" | "error" | "success"; interface ToastItem { id: number; message: string; kind: ToastKind; } type ShowToast = (message: string, kind?: ToastKind) => void; const ToastContext = createContext(null); const TOAST_TTL_MS = 4000; export function ToastProvider({ children }: { children: ReactNode }) { const [items, setItems] = useState([]); // 单调计数器作 id(不再用 Date.now()+random,避免极小概率碰撞且稳定可测)。 const nextIdRef = useRef(0); // 在途定时器集合:卸载时统一清理,避免 setState-after-unmount 泄漏。 const timersRef = useRef>>(new Set()); const show = useCallback((message, kind = "info") => { const id = nextIdRef.current++; setItems((prev) => [...prev, { id, message, kind }]); const timer = setTimeout(() => { timersRef.current.delete(timer); setItems((prev) => prev.filter((t) => t.id !== id)); }, TOAST_TTL_MS); timersRef.current.add(timer); }, []); // 卸载清理所有在途定时器。 useEffect(() => { const timers = timersRef.current; return () => { timers.forEach((t) => clearTimeout(t)); timers.clear(); }; }, []); const value = useMemo(() => show, [show]); return ( {children}
{items.map((t) => (
{t.message}
))}
); } export function useToast(): ShowToast { const ctx = useContext(ToastContext); if (!ctx) { // 容错:未挂 Provider 时退化为 no-op(不应在生产路径发生)。 return () => undefined; } return ctx; }