react - createPortal魔法传送门

文章目录

    • [一、什么是 Portal?](#一、什么是 Portal?)
    • [二、为什么需要 Portal?](#二、为什么需要 Portal?)
      • [1. 解决 CSS 层叠上下文问题](#1. 解决 CSS 层叠上下文问题)
      • [2. 处理全局性 UI 元素](#2. 处理全局性 UI 元素)
      • [3. 保持 DOM 结构合理性](#3. 保持 DOM 结构合理性)
    • [三、核心 API:createPortal](#三、核心 API:createPortal)
    • [四、Portal 的关键特性](#四、Portal 的关键特性)
      • [1. 事件冒泡机制](#1. 事件冒泡机制)
      • [2. 生命周期与上下文](#2. 生命周期与上下文)
    • 五、实战应用场景
      • [1. 模态对话框实现](#1. 模态对话框实现)
      • [2. 全局通知系统](#2. 全局通知系统)
    • 六、性能优化与最佳实践
      • [1. 复用 DOM 节点](#1. 复用 DOM 节点)
      • [2. 避免内存泄漏](#2. 避免内存泄漏)
      • [3. SSR 兼容性处理](#3. SSR 兼容性处理)
    • 七、特别注意

一、什么是 Portal?

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

注意 :

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

js 复制代码
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

基本语法

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

完整示例

js 复制代码
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 树的结构冒泡

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

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

2. 生命周期与上下文

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

js 复制代码
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. 模态对话框实现

js 复制代码
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. 全局通知系统

js 复制代码
const notificationRoot = document.getElementById("notifications");

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

六、性能优化与最佳实践

1. 复用 DOM 节点

js 复制代码
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 节点

js 复制代码
useEffect(() => {
  const div = document.createElement("div");
  document.body.appendChild(div);

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

3. SSR 兼容性处理

js 复制代码
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 组件定位不准确(他不是一定按照浏览器视口进行定位),所以不推荐使用。

相关推荐
要加油哦~4 小时前
AI | 实践教程 - ScreenCoder | 多agents前端代码生成
前端·javascript·人工智能
一个public的class4 小时前
你在浏览器输入一个网址,到底发生了什么?
java·开发语言·javascript
青茶3604 小时前
php怎么实现订单接口状态轮询请求
前端·javascript·php
火车叼位4 小时前
脚本伪装:让 Python 与 Node.js 像原生 Shell 命令一样运行
运维·javascript·python
VT.馒头4 小时前
【力扣】2727. 判断对象是否为空
javascript·数据结构·算法·leetcode·职场和发展
发现一只大呆瓜4 小时前
虚拟列表:从定高到动态高度的 Vue 3 & React 满分实现
前端·vue.js·react.js
鱼毓屿御5 小时前
如何给用户添加权限
前端·javascript·vue.js
JustHappy5 小时前
「web extensions🛠️」有关浏览器扩展,开发前你需要知道一些......
前端·javascript·开源
xixixin_5 小时前
【JavaScript 】从 || 到??:JavaScript 空值处理的最佳实践升级
开发语言·javascript·ecmascript
Java新手村5 小时前
基于 Vue 3 + Spring Boot 3 的 AI 面试辅助系统:实时语音识别 + 大模型智能回答
vue.js·人工智能·spring boot