Skip to content
 

createPortal:魔法传送门

更新: 10/14/2025字数: 0 字 时长: 0 分钟

一、什么是 Portal?

Portal 是 React 提供的一种将子节点渲染到父组件 DOM 层次结构之外的 DOM 节点中的方法。就像魔法世界的"传送门",它允许我们将组件渲染到 DOM 树的任意位置,同时保持其在 React 树中的逻辑位置。

注意

这是一个 API,不是组件,他的作用是:将一个组件渲染到 DOM 的任意位置,跟 Vue 的 Teleport 组件类似。

jsx
import { createPortal } from "react-dom";

function Modal() {
  return createPortal(
    <div className="modal">
      <h2>我是模态框</h2>
      <p>虽然我在这里定义,但我会出现在body末尾!</p>
    </div>
    document.body
  );
}

二、为什么需要 Portal?

1. 解决 CSS 层叠上下文问题

1,当父组件有 overflow: hiddenz-index 时,子组件可能被意外裁剪或遮盖。
2,解决position: fixed存在的一些问题。

2. 处理全局性 UI 元素

模态框、通知、工具、下拉框、全局 loading、提示、等需要突破容器限制。

3. 保持 DOM 结构合理性

将工具提示渲染到触发元素附近可能导致 DOM 结构混乱。

三、核心 API:createPortal

基本语法

jsx
createPortal(children, domNode, key?)
  • children:任何可渲染的 React 子元素
  • domNode:已经存在的 DOM 节点
  • key(可选):用作 portal 的 key

完整示例

jsx
function Tooltip({ children, targetId }) {
  const [isVisible, setIsVisible] = useState(false);
  const targetElement = document.getElementById(targetId);

  if (!targetElement) return null;

  return (
    <>
      {createPortal(
        isVisible && (
          <div className="tooltip">
            {children}
          </div>
        ),
        targetElement
      )}
    </>
  );
}

// 使用
<Tooltip targetId="btn-1">这是一个提示</Tooltip>
<button id="btn-1">悬停我</button>

四、Portal 的关键特性

1. 事件冒泡机制

虽然 DOM 结构不同,但事件仍然按照 React 树的结构冒泡

jsx
function Parent() {
  const handleClick = () => {
    console.log("点击事件从Portal冒泡上来了!");
  };

  return (
    <div onClick={handleClick}>
      <p>父组件</p>
      <Modal /> {/* 使用createPortal渲染到body */}
    </div>
  );
}

2. 生命周期与上下文

Portal 组件完全保留 React 上下文和生命周期

jsx
const ThemeContext = createContext("light");

function ThemedModal() {
  const theme = useContext(ThemeContext);

  useEffect(() => {
    console.log("Modal mounted");
    return () => console.log("Modal unmounted");
  }, []);

  return createPortal(<div className={`modal ${theme}`}>...</div>, document.body);
}

五、实战应用场景

1. 模态对话框实现

jsx
function Modal({ children, onClose }) {
  const modalRoot = useMemo(() => document.createElement("div"), []);

  useEffect(() => {
    document.body.appendChild(modalRoot);
    return () => document.body.removeChild(modalRoot);
  }, [modalRoot]);

  return createPortal(
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-content" onClick={(e) => e.stopPropagation()}>
        {children}
      </div>
    </div>,
    modalRoot
  );
}

2. 全局通知系统

jsx
const notificationRoot = document.getElementById("notifications");

function Notification({ message }) {
  return createPortal(<div className="notification">{message}</div>, notificationRoot);
}

六、性能优化与最佳实践

1. 复用 DOM 节点

jsx
const portalRoot = document.getElementById("portal-root");

function MyPortal({ children }) {
  // 使用useMemo避免重复创建节点
  const container = useMemo(() => document.createElement("div"), []);

  useEffect(() => {
    portalRoot.appendChild(container);
    return () => portalRoot.removeChild(container);
  }, [container]);

  return createPortal(children, container);
}

2. 避免内存泄漏

确保在组件卸载时清理 Portal 节点

jsx
useEffect(() => {
  const div = document.createElement("div");
  document.body.appendChild(div);

  return () => {
    document.body.removeChild(div);
  };
}, []);

3. SSR 兼容性处理

jsx
function SafePortal({ children }) {
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted) return null;

  return createPortal(children, document.body);
}

七、特别注意

推荐使用 createPortal 因为他更灵活,可以挂载到任意位置,而position: fixed,会有很多问题,在默认的情况下他是根据浏览器视口进行定位的,但是如果父级设置了transform、perspective、filter 或 backdrop-filter 属性非 none 时,他就会相对于父级进行定位,这样就会导致 Modal 组件定位不准确(他不是一定按照浏览器视口进行定位),所以不推荐使用。

我见青山多妩媚,料青山见我应如是。