如何解决React+NextJS中带超时配置的Toast随机删除问题
React Context 实现Toast随机异常删除问题修复
问题场景
项目技术栈为 React + NextJS + Tailwind CSS,基于 React Context 封装Toast组件,核心逻辑为:
- 每个Toast支持
timeout配置,值为0时需用户手动关闭,否则到达配置时长(默认3000ms)后自动关闭 - 同时触发自动关闭、手动关闭类型的Toast时,会出现Toast无规律随机被删除的异常
问题相关核心代码如下:
export const ToastContextProvider:FC<ToastProps> = ({ children }) => { const [toasts, setToasts] = useState<ToastComponentProps[]>([]); const generateId = () => Math.random().toString(36).substring(2, 9); const removeToast = (_id: string): void => { setToasts(toasts.filter(({ id }) => id !== _id)); }; const showToast = (toast: ToastComponentProps) => { const toastWithId = { ...toast, id: generateId() }; setToasts((oldToasts) => [...oldToasts, toastWithId]); if (toast.timeout !== 0) { setTimeout(() => removeToast(toastWithId.id), toast.timeout ?? 3000); } }; // 其余上下文逻辑省略 }
问题根因
异常来自React闭包陷阱:
removeToast方法中更新状态时,直接使用了当前渲染周期闭包捕获的toasts变量,没有采用函数式更新获取最新状态- 连续触发多个Toast时,不同自动关闭定时器触发时,调用的
removeToast持有的是对应渲染节点的旧toasts快照,执行filter操作时会丢失快照生成后新增的Toast,相当于用旧的状态覆盖了最新的列表,最终表现为Toast随机被错删、漏删。
修复代码
核心修复点是将removeToast中的状态更新改为函数式写法,保证每次执行删除时都能拿到最新的Toast列表,可选优化是存储定时器ID、用useCallback包装方法减少不必要的上下文重渲染:
import { useState, useCallback, useRef, useEffect } from 'react'; export const ToastContextProvider:FC<ToastProps> = ({ children }) => { const [toasts, setToasts] = useState<ToastComponentProps[]>([]); // 存储定时器ID,组件卸载时清理避免内存泄漏 const timerRef = useRef<Record<string, NodeJS.Timeout>>({}); const generateId = () => Math.random().toString(36).substring(2, 9); // 函数式更新获取最新状态,不依赖闭包捕获的toasts值 const removeToast = useCallback((_id: string): void => { setToasts((latestToasts) => latestToasts.filter(({ id }) => id !== _id)); // 清理对应定时器 if (timerRef.current[_id]) { clearTimeout(timerRef.current[_id]); delete timerRef.current[_id]; } }, []); const showToast = useCallback((toast: ToastComponentProps) => { const toastWithId = { ...toast, id: generateId() }; setToasts((oldToasts) => [...oldToasts, toastWithId]); if (toast.timeout !== 0) { const timer = setTimeout(() => removeToast(toastWithId.id), toast.timeout ?? 3000); timerRef.current[toastWithId.id] = timer; } }, [removeToast]); // 组件卸载时清空所有定时器 useEffect(() => { return () => { Object.values(timerRef.current).forEach(clearTimeout); timerRef.current = {}; } }, []) // 其余上下文传递逻辑省略 }
修复后不管同时触发多少个不同配置的Toast,删除逻辑都会基于最新的列表执行,不会再出现随机错删的问题。
内容的提问来源于stack exchange,提问作者dreeeeeedd
相关产品推荐
相关产品推荐

