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

相关推荐
苦藤新鸡1 天前
27.合并有序链表,串葫芦
前端·javascript·链表
_OP_CHEN1 天前
【前端开发之HTML】(四)HTML 标签进阶:表格、表单、布局全掌握,从新手到实战高手!
前端·javascript·css·html·html5·网页开发·html标签
Alair‎1 天前
前端开发之环境配置
前端·react.js
谢尔登1 天前
Vue3底层原理——keep-alive
javascript·vue.js·ecmascript
Deca~1 天前
VueVirtualLazyTree-支持懒加载的虚拟树
前端·javascript·vue.js
2501_944526421 天前
Flutter for OpenHarmony 万能游戏库App实战 - 主题切换实现
android·开发语言·javascript·python·flutter·游戏·django
爱上妖精的尾巴1 天前
7-11 WPS JS宏 对象的属性值为函数的写法与用法
前端·javascript·wps·js宏·jsa
zuozewei1 天前
零基础 | 使用LangChain框架实现ReAct Agent
前端·react.js·langchain
爱上妖精的尾巴1 天前
7-12 WPS JS宏 this、return用构造函数自定义类-1:对象内部函数,外部调用的写法
前端·javascript·wps·js宏·jsa
白鳯1 天前
分形世界:React实现交互式分形图绘制与导出
react.js·前端框架·cursor·分形·vibe coding·分形绘制·数学之美