React Portal 实战:用 createPortal 把弹窗渲染到 body 脱离父级裁剪
test12026-09-261 次阅读
为什么需要 Portal
在 React 里,组件返回的 JSX 默认会挂到父组件的 DOM 树里。当弹窗、Toast、下拉菜单被放在一个设置了 overflow: hidden 或 position: relative 的容器内部时,子元素很容易被父级裁掉,或者被嵌套的 z-index 挡住。这类"脱离文档流但又想留在 React 树里"的节点,用 Portal 最合适。
ReactDOM.createPortal 把子节点渲染到指定的 DOM 容器(通常是 document.body),但事件仍然沿着 React 组件树冒泡,不会因为 DOM 位置变了就丢失回调。
基本用法
import { createPortal } from 'react-dom';
function Tooltip({ children, target }) {
return createPortal(
<div className="tooltip">{children}</div>,
target // 任意已存在的 DOM 节点
);
}
封装一个可复用的 Modal
把挂载节点的创建、滚动锁定、Esc 关闭都收敛到一个组件里,业务侧只传内容即可。
import { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
export function Modal({ open, onClose, title, children }) {
const [el] = useState(() => document.createElement('div'));
useEffect(() => {
if (!open) return;
document.body.appendChild(el);
document.body.style.overflow = 'hidden';
const onKey = (e) => { if (e.key === 'Escape') onClose(); };
window.addEventListener('keydown', onKey);
return () => {
document.body.removeChild(el);
document.body.style.overflow = '';
window.removeEventListener('keydown', onKey);
};
}, [open, el, onClose]);
if (!open) return null;
return createPortal(
<div className="mask" onClick={onClose}>
<div className="dialog" onClick={(e) => e.stopPropagation()}>
<h3>{title}</h3>
<div>{children}</div>
</div>
</div>,
el
);
}
事件冒泡的注意点
Portal 子节点虽然在 document.body 下,但在 React 看来它仍然属于声明它的组件。因此点击弹窗里的按钮,onClick 会按 React 组件树冒泡,而不是原生 DOM 树。这意味着父组件的捕获/冒泡逻辑照常生效,不必用原生 addEventListener 去 document 上补监听。
SSR 与多 Portal
在 SSR(如 Next.js)场景下,服务端没有 document,应在 useEffect 里创建挂载节点,组件首次渲染返回 null,避免服务端/客户端不一致。多个 Portal 共用 document.body 没有问题,各自维护自己的挂载节点即可,卸载时记得同步移除。
- 弹窗、抽屉、全局通知优先用 Portal,避免父级样式污染。
- 挂载节点随组件生命周期创建和销毁,不要长期遗留空 div 在 body。
- 遮罩层关闭与内容区点击要区分,避免点内容误关弹窗。
T
test1
文章作者
为什么需要 Portal 在 React 里,组件返回的 JSX 默认会挂到父组件的 DOM 树里。当弹窗、Toast...
- 分类
- 技术
- 发布时间
- 2026-09-26
- 字数
- 约 1919 字
- 阅读
- 1 次
