Published on

createPortal():解耦逻辑与物理 DOM

在 React 应用中,组件的渲染通常与其在组件树中的层级结构相对应。然而,对于某些特殊的 UI 元素,如模态框 (Modal)、工具提示 (Tooltip) 或下拉菜单 (Dropdown),它们需要在视觉上“脱离”其父组件的容器,在 DOM 的最顶层进行渲染。如果直接在组件内部渲染,可能会受到父组件 overflow: hiddenz-indextransform 等 CSS 属性的限制。ReactDOM.createPortal() 正是 React 提供的官方解决方案,它允许开发者在保留组件在 React 树中逻辑位置的同时,将其渲染内容物理地放置到 DOM 树中的任意指定节点下。

createPortal 的核心机制

语法与参数

createPortalreact-dom 模块下的一个 API,其基本语法如下:

ReactDOM.createPortal(children, domNode, key?)
  • children: 任何可渲染的 React 子元素,如 JSX 元素、字符串或数组。
  • domNode: 一个已经存在的 DOM 元素,你的 children 将被渲染到这个 DOM 节点下。这通常是 document.body 或一个专用的 div 容器。
  • key (可选): 一个字符串或数字,用于 React 的列表渲染优化,其作用与普通的 key 属性相同。
逻辑位置 vs. 物理位置

Portal 机制的核心在于将一个组件的逻辑位置(它在组件树中的位置)与它的物理位置(它在真实 DOM 树中的位置)分离。

createPortal 使得组件能够保持其在 React 树中的位置,从而继续访问其祖先组件的状态 (state)、属性 (props) 和上下文 (context)。

如上图所示,ModalComponent 在 React 树中是嵌套的,但其 DOM 节点却被渲染到了一个完全不同的位置。

核心特性:事件冒泡与上下文

createPortal 最为精妙的设计在于它如何处理事件冒泡。

  • 事件冒泡 (Event Bubbling): 尽管 Portal 的子元素在 DOM 树中处于一个不同的位置,但其内部触发的事件,仍然会遵循 React 组件树的层级结构进行冒泡,而不是物理 DOM 树。这意味着一个位于 Portal 中的按钮,其 onClick 事件依然可以被其在 React 树中的祖先组件捕获。
  • 上下文 (Context): Portal 的子组件可以正常访问其在 React 树中的祖先 Provider 所提供的任何上下文值。

典型应用场景与最佳实践

解决模态框的布局问题

这是 createPortal 最经典的用例。模态框通常需要在页面的顶层显示,以覆盖所有其他内容。

  • 问题: 如果不使用 Portal,模态框的 DOM 可能会被其父组件的 overflow: hidden;position 样式裁剪,或者需要复杂的 z-index 管理才能确保它始终在最上层。
  • 解决方案: createPortal 允许我们将模态框的 DOM 节点直接渲染到 document.body 或一个预先创建的顶层容器中。这样,模态框就不再受父组件布局的限制,能够轻松地覆盖整个屏幕,并且通过简单的 z-index 就能保证其层级。
createPortal 实现 Modal

// 创建一个顶层 DOM 容器
const modalRoot = document.getElementById('modal-root');
if (!modalRoot === null) {
  const newModalRoot = document.createElement('div');
  newModalRoot.id = 'modal-root';
  document.body.appendChild(newModalRoot);
}

// Modal 组件
function Modal({ children, isOpen }) {
  if (!isOpen) return null;

  return ReactDOM.createPortal(
    // 将 Modal 内容渲染到 modal-root 容器
    <div className="modal-backdrop">
      <div className="modal-content">
        {children}
      </div>
    </div>,
    document.getElementById('modal-root')
  );
}

// 使用 Modal 组件
function App() {
  const [isModalOpen, setIsModalOpen] = useState(false);
  return (
    <div>
      <h1>Portal 示例</h1>
      <button onClick={() => setIsModalOpen(true)}>打开 Modal</button>
      <Modal isOpen={isModalOpen}>
        <h2>这是一个 Modal</h2>
        <p>Modal 内容被渲染到 DOM 顶层,不会被父组件影响。</p>
        <button onClick={() => setIsModalOpen(false)}>关闭</button>
      </Modal>
    </div>
  );
}

可访问性 (Accessibility)

在使用 Portal 实现模态框时,开发者必须手动处理焦点管理。例如,要确保键盘用户无法将焦点移出模态框,并且在模态框关闭后,将焦点返回到触发打开的那个元素上。