555 字
3 分钟
React 的 Portal

它解决什么#

弹窗、下拉菜单、悬浮提示常被父级挡住:祖先元素上有 overflow: hidden,或者父级建立了层叠上下文,z-index 再高也出不来。

Portal 把这段内容渲染到另一个 DOM 节点下,通常是 document.body。

用 createPortal#

import { createPortal } from 'react-dom'
function Modal({ children, onClose }) {
return createPortal(
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
{children}
</div>
</div>,
document.body
)
}

API 是 createPortal(children, domNode, key?),从 react-dom 导入,不是从 react(旧教程里那句「18 要配 createRoot、17 直接挂载」是错的,它一直没变过)。

目标节点必须已经在 DOM 里。想挂到自定义容器上,就在 HTML 里先放一个 <div id="portal-root">。

它只改 DOM 位置,不改组件树#

Portal 里的组件仍然读得到父级的 context,照常接收 props;事件也按 React 组件树冒泡,不走 DOM 树。

所以「点在弹窗内部,外层组件的 onClick 也触发了」是正常行为,不是 bug。要拦住就在内层元素上 e.stopPropagation(),上面的例子正是这么写的。

样式与层级要自己盯#

类名不受影响,CSS Modules 之类按类名加作用域的方案照常生效。会失效的是依赖祖先结构的选择器:

/* Portal 之后对不上了:弹窗已经不在 .card 里面 */
.card .modal { /* ... */ }
/* 仍然生效 */
.modal { /* ... */ }

层级也要自己排:遮罩现在和 body 下的其他元素平级,z-index 得单独定一套规则。

键盘与焦点要自己管#

Portal 不提供焦点陷阱,也不管 Esc 关闭、打开时焦点落在哪、关闭后焦点回到哪。这四件事都要写。

如果目标浏览器版本允许,直接用原生 <dialog> 元素更省事:模态、焦点陷阱与 Esc 关闭由浏览器提供。Portal 仍然适合下拉菜单、悬浮提示这类不需要模态语义的场合。

服务端渲染#

document 在服务端不存在,别在渲染期间读它。常见写法是先用 state 存住容器,等挂载后再取:

const [container, setContainer] = useState(null)
useEffect(() => {
setContainer(document.getElementById('portal-root'))
}, [])
if (!container) return null
return createPortal(children, container)

这样首次渲染输出 null,服务端与客户端的水合结果一致。

第 9 篇,共 39 篇本文属于系列

React 的 Portal
https://www.choria.top/posts/portal/
作者
乔瑞雅·克林威尔
发布于
2026-05-03
许可协议
CC BY-NC-SA 4.0

分享文章

生成精美分享图或复制链接,与更多人分享本文。

继续阅读

沿着主题读

基于共同的标签与分类

换条路线

从其他文章中稳定抽取

最后更新于 ,距今已过 189 天

部分内容可能已过时