React传送门createPortal

大家好,我是小杨,从事前端开发快6年了。今天想和大家聊聊React中的一个特别有趣的功能------createPortal。记得刚入行那会儿,React还没有这个API,为了实现类似功能,我们可是费了不少劲。现在回想起来,真是感慨技术迭代的速度啊!

一、什么是createPortal?

简单来说,createPortal就像是React世界里的"任意门"。它允许我们将子组件渲染到父组件DOM层次结构之外的DOM节点上,但却保持它们在React树中的逻辑关系。

先来看个简单的代码示例:

jsx 复制代码
import { useRef } from 'react';
import { createPortal } from 'react-dom';

function MyComponent() {
  const modalRoot = document.getElementById('modal-root');
  const modalRef = useRef(null);
  
  // 使用createPortal将模态框内容渲染到modal-root中
  return createPortal(
    <div className="modal" ref={modalRef}>
      <h2>Hello from Portal!</h2>
      <p>This is rendered outside the main component tree!</p>
    </div>,
    modalRoot
  );
}

在这个例子中,虽然模态框的JSX写在MyComponent组件里,但它实际上会被渲染到id为modal-root的DOM节点中。

二、为什么需要Portal?使用场景揭秘

在我早期的项目中,遇到过几个必须使用Portal的场景:

1. 模态框(Modal)和对话框(Dialog)

这是最经典的用例。模态框需要覆盖整个页面,如果嵌套在深层组件中,z-index和overflow:hidden会导致样式问题。

2. 工具提示(Tooltip)和弹出菜单(Popover)

当组件在滚动容器内时,工具提示可能会被裁剪,Portal可以确保它们始终显示在正确位置。

3. 全局通知(Toast)系统

通知需要显示在页面顶层,不受父组件样式影响。

三、没有Portal的黑暗时代

记得2018年我做的一个后台管理系统,需要实现一个全局模态框。当时React还没有createPortal(或者我们还没意识到它的存在),我是这么做的:

jsx 复制代码
class OldSchoolModal extends React.Component {
  constructor(props) {
    super(props);
    this.modalContainer = document.createElement('div');
    this.modalContainer.id = 'modal-container';
  }

  componentDidMount() {
    document.body.appendChild(this.modalContainer);
  }

  componentWillUnmount() {
    document.body.removeChild(this.modalContainer);
  }

  render() {
    return ReactDOM.createReactRoot(this.modalContainer).render(
      <div className="modal-overlay">
        <div className="modal-content">
          {this.props.children}
        </div>
      </div>
    );
  }
}

这种方式有几个明显的问题:

  1. 生命周期管理复杂:需要手动处理DOM节点的创建和销毁
  2. 上下文丢失:Portal外的组件无法共享React上下文(Context)
  3. 事件冒泡异常:事件不会按照React树结构冒泡
  4. 代码冗余:每个需要此功能的组件都要写重复代码

四、有Portal后的优雅实现

现在有了createPortal,一切都变得简单了:

jsx 复制代码
import { useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';

function ModernModal({ children, isOpen }) {
  const modalRoot = document.getElementById('modal-root');
  const elementRef = useRef(document.createElement('div'));

  useEffect(() => {
    const element = elementRef.current;
    modalRoot.appendChild(element);
    
    return () => {
      modalRoot.removeChild(element);
    };
  }, [modalRoot]);

  if (!isOpen) return null;

  return createPortal(
    <div className="modal-overlay">
      <div className="modal-content">
        {children}
      </div>
    </div>,
    elementRef.current
  );
}

五、Portal带来的巨大优势

  1. 保持React上下文:Portal内的组件可以访问父树的Context和状态
  2. 正确的事件冒泡:尽管DOM结构不同,但事件会按照React组件树冒泡
  3. 更好的可维护性:代码更简洁,逻辑更清晰
  4. TypeScript友好:完整的类型支持

六、实际项目中的小技巧

在我的日常开发中,总结了几个使用Portal的实用技巧:

jsx 复制代码
// 1. 自定义Hook封装
function usePortal(id) {
  const portalRef = useRef(null);
  
  useEffect(() => {
    let element = document.getElementById(id);
    if (!element) {
      element = document.createElement('div');
      element.id = id;
      document.body.appendChild(element);
    }
    portalRef.current = element;
    
    return () => {
      if (element && element.parentNode) {
        element.parentNode.removeChild(element);
      }
    };
  }, [id]);
  
  return portalRef.current;
}

// 2. 在组件中使用
function SmartModal({ children }) {
  const portalNode = usePortal('modal-root');
  
  if (!portalNode) return null;
  
  return createPortal(
    <div className="smart-modal">
      {children}
    </div>,
    portalNode
  );
}

七、注意事项

虽然Portal很强大,但使用时要注意:

  • 服务端渲染(SSR)时需要特殊处理
  • 确保目标DOM节点存在
  • 注意内存泄漏,及时清理

⭐ 写在最后

请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.

✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式

✅ 认为我部分代码过于老旧,可以提供新的API或最新语法

✅ 对于文章中部分内容不理解

✅ 解答我文章中一些疑问

✅ 认为某些交互,功能需要优化,发现BUG

✅ 想要添加新功能,对于整体的设计,外观有更好的建议

✅ 一起探讨技术加qq交流群:906392632

最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!

相关推荐
mumuWorld1 分钟前
解决openclaw以及插件安装的报错
前端·ai编程
GISer_Jing2 分钟前
前端组件库——shadcn/ui:轻量、自由、可拥有,解锁前端组件库的AI时代未来
前端·人工智能·ui
执行部之龙6 分钟前
JS手写——call bind apply
前端·javascript
京东零售技术7 分钟前
告别手动搬砖: JoyCode + i18n-mcp 实现前端项目多语言自动化
前端
李少兄7 分钟前
企业资源计划(ERP)系统全景指南
java·前端·数据库·erp
张一凡9311 分钟前
React 项目也能用依赖注入?我尝试了一下,真香
前端·react.js
somebody12 分钟前
零经验学 react 的第15天 - 过渡动画(使用 react-transition-group 库进行实现)
前端
吴声子夜歌21 分钟前
JavaScript——函数
开发语言·javascript·ecmascript
SuperEugene26 分钟前
Vue3 + Element Plus 表单开发实战:防重复提交、校验、重置、loading 统一|表单与表格规范篇
前端·javascript·vue.js
SuperEugene28 分钟前
Vue3 + Element Plus 中后台弹窗规范:开闭、传参、回调,告别弹窗地狱|Vue 组件与模板规范篇
开发语言·前端·javascript·vue.js·前端框架