使用Radix-UI创建Toast服务时无法修改open状态的问题
Radix-UI Toast状态修改失效问题解决方案
问题描述
我基于Radix-UI构建Toast服务时遇到了状态更新失效的问题:无法通过Toast对象内部的setOpen钩子修改open状态。
我的Toast服务工作流程:
- 应用内任意组件可向全局store添加Toast实例
Toastr.tsx监听store中的toasts数据列表- 新增
ToastrItem对象时自动渲染对应的Toast组件 - 默认5秒后调用
onOpenChange回调,尝试将open设为false,但该操作完全无效
无效代码片段:
onOpenChange={(openStatus: boolean) => { // 此代码无法更新open状态 toast.setOpen(openStatus) }}
问题根源
问题出在getToast.tsx的实现逻辑上,核心错误是在普通函数中调用React Hook,导致状态脱离组件生命周期管理。相关代码如下:
getToast.tsx
export const getToast = () => { const toast = new ToastObject() as ToastrItem; // 错误:useState在普通函数中调用,脱离React组件生命周期 const [open, setOpen] = useState(false); toast.open = open; toast.setOpen = setOpen; return toast; }; class ToastObject implements ToastrItem { id: string; title: string; description: string; variant: ToastrVariant; open: boolean; setOpen: (param: boolean) => void; constructor() { this.id = nanoid(); this.title = 'test'; this.description = 'description'; this.variant = ToastrVariant.GOOD; this.open = true; this.setOpen = () => false; } }
ToastrItem.tsx
export interface ToastrItem { id: string; title: string; description: string; variant: ToastrVariant; open: boolean; setOpen: (parameter: boolean) => any; }
React Hook规则明确要求:Hook必须在组件的顶层作用域调用,不能在普通函数、类或循环中使用。你现在把useState放在getToast这个普通函数里,导致该状态完全不受React生命周期管控,调用setOpen自然不会触发组件重渲染,状态更新也就无效。
修复方案
方案1:全局store统一管理状态
将所有Toast的open状态托管到全局store,通过store方法更新状态:
// 全局Toast Store示例 const toastStore = createStore({ toasts: [] as ToastrItem[], // 添加Toast时初始化open状态为true addToast: (toast: Omit<ToastrItem, 'open' | 'setOpen'>) => { toastStore.setState({ toasts: [...toastStore.getState().toasts, { ...toast, id: nanoid(), open: true }] }); }, // 通过id更新指定Toast的open状态 updateToastOpen: (id: string, open: boolean) => { toastStore.setState({ toasts: toastStore.getState().toasts.map(t => t.id === id ? { ...t, open } : t ) }); } });
在ToastrItem组件中使用store状态:
const ToastrItem = ({ toast }: { toast: ToastrItem }) => { const { updateToastOpen } = toastStore.useStore(); // 自动关闭定时器 useEffect(() => { const timer = setTimeout(() => updateToastOpen(toast.id, false), 5000); return () => clearTimeout(timer); }, [toast.id, updateToastOpen]); return ( <Toast.Root open={toast.open} onOpenChange={(open) => updateToastOpen(toast.id, open)} > <Toast.Title>{toast.title}</Toast.Title> <Toast.Description>{toast.description}</Toast.Description> </Toast.Root> ); };
方案2:组件内部维护状态
如果不需要全局store,可在渲染Toast的组件内部独立维护每个Toast的open状态:
// Toastr.tsx(Toast容器组件) const Toastr = () => { const [toasts, setToasts] = useState<Omit<ToastrItem, 'open' | 'setOpen'>[]>([]); // 对外暴露添加Toast的方法 const addToast = (toast: Omit<ToastrItem, 'open' | 'setOpen'>) => { setToasts(prev => [...prev, { ...toast, id: nanoid() }]); }; return ( <Toast.Provider> {toasts.map(toast => ( <ToastItem key={toast.id} toast={toast} /> ))} </Toast.Provider> ); }; // ToastItem.tsx(单个Toast组件) const ToastItem = ({ toast }: { toast: Omit<ToastrItem, 'open' | 'setOpen'> }) => { const [open, setOpen] = useState(true); // 5秒后自动关闭 useEffect(() => { const timer = setTimeout(() => setOpen(false), 5000); return () => clearTimeout(timer); }, []); return ( <Toast.Root open={open} onOpenChange={setOpen}> <Toast.Title>{toast.title}</Toast.Title> <Toast.Description>{toast.description}</Toast.Description> </Toast.Root> ); };
内容的提问来源于stack exchange,提问作者DeveloperGilfoyle
相关产品推荐
相关产品推荐

