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 次