如何使用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
相关产品推荐
相关产品推荐

