You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.25 17:18:26