尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Next.js 实战:使用 React Portals 将 Modal 弹窗渲染到任意 DOM 节点

Next.js 实战:使用 React Portals 将 Modal 弹窗渲染到任意 DOM 节点 Next.js 实战使用 React Portals 将 Modal 弹窗渲染到任意 DOM 节点【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js本文基于 Next.js 仓库中的官方示例with-portals讲解如何在 Next.js 页面中正确使用 React 的createPortal能力通过自定义ClientOnlyPortal包装组件解决 SSR 环境下 Portal 的挂载时机问题并借助自定义_document.js在body下预留挂载节点最终实现一个可开关的模态弹窗。读完本文你将掌握 Portal 在 Next.js 中的完整接线方式、客户端专属渲染client-only的安全模式以及 Next.js 框架内部自身使用 Portal 的源码证据。这个示例要解决什么问题在 React 中组件的 DOM 输出通常严格受限于父级组件的 DOM 层级。而ReactDOM.createPortalReact 官方文档中的 Portals 特性示例源码内即引用该文档允许把子树穿透渲染到 React 组件树之外的任意 DOM 节点上。典型场景就是模态弹窗弹窗的 DOM 需要挂在body根部才能避开页面局部 CSSoverflow、transform、contain等对position: fixed定位和层叠上下文stacking context的干扰但它的 React 状态逻辑仍保留在触发它的组件内部。with-portals示例正是围绕这一场景示例首页 只渲染一个Modal /点击 Open Modal 按钮后弹窗通过 Portal 渲染到_document.js中预先声明的div idmodal /节点关闭按钮则将其卸载。项目结构整个示例仅由少量文件构成README 描述了使用方式各文件职责如下文件职责examples/with-portals/pages/index.js首页仅渲染Modal /examples/with-portals/pages/_document.js自定义 Document在body内声明 Portal 挂载点examples/with-portals/components/ClientOnlyPortal.js核心客户端专属的 Portal 包装组件examples/with-portals/components/Modal.js弹窗 UI开关状态、遮罩层、样式examples/with-portals/package.json依赖声明next取latestreact/react-dom为^18.2.0快速开始README 中给出三种包管理器下通过create-next-app引导示例的命令可任选其一复制运行npx create-next-app --example with-portals with-portals-appyarn create next-app --example with-portals with-portals-apppnpm create next-app --example with-portals with-portals-app引导完成后示例 package.json 提供标准脚本可用npm run dev启动开发服务器npm run build与npm run start执行构建和生产启动。核心实现拆解第一步在_document.js中预留挂载节点Portal 需要一个真实存在于 DOM 中的容器。示例通过 Pages Router 的 Document 组件在body内、Main /之后硬编码了一个空节点// examples/with-portals/pages/_document.js import { Html, Head, Main, NextScript } from next/document; export default function Document() { return ( Html Head / body Main / {/* Here we will mount our modal portal */} div idmodal / NextScript / /body /Html ); }要点有两处其一div idmodal /是静态 HTML无论客户端是否触发弹窗该节点都会在首屏 HTML 中就绪Portal 打开时直接可用无需运行时动态创建其二它位于body之下、页面内容之后天然处于顶层 DOM 位置避开了页面布局组件的样式约束。这里使用_document.js而非在组件里动态document.createElement也顺带避免了动态创建节点与 React 卸载时序之间可能产生的内存泄漏问题。第二步ClientOnlyPortal—— 解决 SSR 下的 Portal 安全问题ClientOnlyPortal.js 是示例的核心全部实现如下import { useRef, useEffect, useState } from react; import { createPortal } from react-dom; export default function ClientOnlyPortal({ children, selector }) { const ref useRef(); const [mounted, setMounted] useState(false); useEffect(() { ref.current document.querySelector(selector); setMounted(true); }, [selector]); return mounted ? createPortal(children, ref.current) : null; }为什么必须这样写关键在于执行环境的差异服务端没有document。在 Next.js 的服务端渲染阶段document.querySelector(selector)会直接因document未定义而抛出异常即便用可选链规避也拿不到任何 DOM 节点。因此查询挂载节点的动作被放进useEffect——React 保证它只在客户端挂载后执行。首帧不能返回 Portal 结果。组件用mounted状态标记已具备客户端环境在服务端渲染和客户端首次 hydrate 时都返回null避免服务端输出与客户端首帧输出不一致而触发 hydration 错误待useEffect执行、ref.current指向真实的#modal节点并setMounted(true)后下一次渲染才调用createPortal(children, ref.current)。这个mounted 标志位 条件渲染的组合是 React 社区处理浏览器专属 API 的通用模式示例把它收敛成一个可复用的selector参数化组件调用方只关心往哪个节点投射ClientOnlyPortal selector#modal {/* 弹窗内容 */} /ClientOnlyPortal第三步Modal组件负责状态与表现Modal.js 用useState管理open状态仅在open为真时渲染 Portal 子树关闭即从#modal节点卸载button typebutton onClick{() setOpen(true)}Open Modal/button {open ( ClientOnlyPortal selector#modal div classNamebackdrop div classNamemodal pThis modal is rendered using portals./p button typebutton onClick{() setOpen(false)}Close Modal/button /div /div /ClientOnlyPortal )}样式部分使用 styled-jsx其中有一个值得注意的细节通过:global(body)在弹窗打开期间将body的overflow设为hidden防止背景页面滚动——由于弹窗关闭时整个 Portal 子树连同该style块被卸载这一全局样式也随之自动恢复不需要额外清理逻辑。.backdrop使用position: fixed铺满视口.modal使用position: absolute并四边各留 10% 形成居中弹窗。为什么这个模式值得记住Portal 在 React 中只是换个 DOM 位置渲染但接入 Next.js 这类 SSR 框架时三个环节必须同时正确挂载节点必须出现在服务端输出的 HTML 中本例由_document.js保证节点查询必须推迟到客户端useEffect中执行document.querySelector首帧输出必须与 SSR 输出保持一致mounted为false时返回null。任何一步缺失都会在 SSR 构建时抛错或产生 hydration 警告。ClientOnlyPortal把这三点封装成了约 15 行的可复用组件这正是该示例的教学价值所在。延伸Next.js 框架自身也在用 Portal从源码结构看Portal 并非只是用户侧技巧Next.js 客户端运行时内部同样依赖它。例如 packages/next/src/client/portal/index.tsx 定义了一个内部Portal组件在useEffect中动态document.createElement(type)并挂到document.body组件卸载时removeChild清理然后用createPortal把子节点投射进去——与示例中的思路同构只是挂载节点改为运行时创建该 Portal 被 packages/next/src/client/components/app-router-announcer.tsx 等客户端组件复用。这说明客户端建节点 Portal 投射是被框架本身验证过的成熟做法。此外Next.js 在 packages/next/src/compiled/react-dom 目录下打包了react-dom的编译产物createPortal即来源于此开发者无需在 Next.js 项目中额外安装任何依赖即可获得该能力。关键要点与适用前提该示例基于Pages Routerpages/目录挂载节点通过_document.js声明若项目使用 App Router可改为在layout.tsx的body内声明同名节点ClientOnlyPortal部分可原样复用。依赖版本以 package.json 为准react/react-dom为^18.2.0next取latestcreatePortal的 API 形态在 React 18/19 中保持一致。若生产场景要进一步完善弹窗体验可在 Portal 内部补充焦点管理打开时聚焦弹窗、关闭后归还焦点、Esc关闭、aria-modal与roledialog等无障碍属性——这些属于通用 React 弹窗实践当前示例未包含。参考仓库内其他示例with-portals-ssr 展示了另一种服务端也渲染 Portal 内容的变体思路可与本文的客户端专属模式对比理解。【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表