
它解决什么
弹窗、下拉菜单、悬浮提示常被父级挡住:祖先元素上有 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 nullreturn createPortal(children, container)这样首次渲染输出 null,服务端与客户端的水合结果一致。
分享文章
生成精美分享图或复制链接,与更多人分享本文。
继续阅读
换条路线
从其他文章中稳定抽取
最后更新于 ,距今已过 189 天
部分内容可能已过时