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

如何使用Storybook预览React Portal组件?

解决Storybook中React Portal找不到目标元素的问题

问题分析

你当前的代码在普通页面正常,但Storybook里首次渲染失败,核心原因是Component.tsx在模块加载阶段就执行了document.getElementById("example-root"),此时Storybook还没渲染出Story里的<div id="example-root">,导致exampleRoot为null;热更新时元素已经存在,所以能正常渲染。

解决方案

方案1:将元素获取逻辑移到组件内部(推荐)

不要在组件外部提前获取DOM元素,而是在组件挂载后通过useEffect获取,同时处理元素不存在的边界情况:

// Component.tsx
import { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';

function Component() {
  const [targetEl, setTargetEl] = useState<HTMLElement | null>(null);

  useEffect(() => {
    // 组件挂载后再查找目标元素
    const el = document.getElementById("example-root");
    setTargetEl(el);
  }, []);

  // 元素不存在时返回null或加载状态
  if (!targetEl) return null;

  return ReactDOM.createPortal(<div>...</div>, targetEl);
}

方案2:在Storybook全局预览中添加目标元素

如果多个Story都需要用到#example-root,可以直接在Storybook的preview.html(通常在.storybook目录下)中添加这个元素,这样所有Story渲染前就已经存在该元素:

<!-- .storybook/preview.html -->
<!DOCTYPE html>
<html>
  <body>
    <div id="example-root"></div>
  </body>
</html>

之后你的Stories.tsx就可以简化,不用再重复添加<div id="example-root">:

// Stories.tsx
const ExampleStory = () => <Component />;

方案3:在Story中控制渲染时机

如果不想修改组件代码,可以在Story中通过状态控制,确保目标元素渲染完成后再加载Component:

// Stories.tsx
import { useEffect, useState } from 'react';

const ExampleStory = () => {
  const [ready, setReady] = useState(false);

  useEffect(() => {
    // 确保元素已挂载
    setReady(true);
  }, []);

  return (
    <>
      <div id="example-root"></div>
      {ready && <Component />}
    </>  
  )
}

关键原理

React Portal要求目标DOM元素在Portal渲染时必须存在。在Storybook的渲染流程中,模块级别的代码会在Story的JSX渲染前执行,所以提前获取元素必然会失败。将元素获取逻辑延迟到组件挂载后(或Story渲染完成后),就能保证目标元素已存在。

内容的提问来源于stack exchange,提问作者Luiz Meraki

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 23:10:55