30-seconds-of-code 之 usePortal:用自定义 Hook 在 React 中程序化创建 Portal

发布时间:2026/10/6 12:09:07

30-seconds-of-code 之 usePortal:用自定义 Hook 在 React 中程序化创建 Portal 教程文档【免费下载链接】30-seconds-of-codeCoding articles to level up your development skills项目地址https://gitcode.com/gh_mirrors/30/30-seconds-of-code点击查看免费下载导读本文基于 usePortal Hook 文档讲解如何用useState()、useCallback()与useEffect()三个基础 Hook 组合出usePortal自定义 Hook实现程序化创建 React Portal将子组件渲染到父组件 DOM 树之外的任意节点。读完本文你将掌握 Portal 的原理、动态挂载/卸载 DOM 节点的完整流程并能在弹窗、Tooltip、全局提示等场景中直接复用这段实现。一、为什么需要程序化的 PortalReact 官方提供的 Portal 能力ReactDOM.createPortal()可以把 JSX 渲染到指定的 DOM 容器中从而突破子组件只能渲染在父组件内部的层级限制。但常规用法需要在 JSX 中显式声明Portal并预先指定目标节点这在以下场景中不够灵活目标容器是动态创建的例如运行时才生成的div希望在 Hook 层面管理挂载与卸载的完整生命周期需要把渲染到何处的决策从组件 JSX 中抽离出来交给调用方。usePortal的设计目标正是解决这个问题它接收一个 DOM 元素el返回一个可直接在 JSX 中使用的渲染组件并负责在依赖变化时自动完成旧 Portal 的卸载与新 Portal 的创建。二、完整实现代码仓库文档给出的核心实现如下保存于 use-portal.mdconst usePortal el { const [portal, setPortal] React.useState({ render: () null, remove: () null, }); const createPortal React.useCallback(el { const Portal ({ children }) ReactDOM.createPortal(children, el); const remove () ReactDOM.unmountComponentAtNode(el); return { render: Portal, remove }; }, []); React.useEffect(() { if (el) portal.remove(); const newPortal createPortal(el); setPortal(newPortal); return () newPortal.remove(el); }, [el]); return portal.render; }; const App () { const Portal usePortal(document.querySelector(title)); return ( p Hello world! PortalPortalized Title/Portal /p ); }; ReactDOM.createRoot(document.getElementById(root)).render( App / );三、实现逐步拆解1. 用useState()保存 Portal 的渲染器与清理器const [portal, setPortal] React.useState({ render: () null, remove: () null, });初始状态下render与remove都是空函数返回null/ 无操作保证第一次渲染时 Hook 不会产生副作用。render是后续真正挂载到 DOM 的组件remove用于卸载 Portal。这种把一对函数作为状态保存的模式让 Portal 的创建逻辑对组件渲染过程完全透明。2. 用useCallback()记忆化createPortalconst createPortal React.useCallback(el { const Portal ({ children }) ReactDOM.createPortal(children, el); const remove () ReactDOM.unmountComponentAtNode(el); return { render: Portal, remove }; }, []);Portal组件内部调用ReactDOM.createPortal(children, el)把children渲染进目标节点elremove调用ReactDOM.unmountComponentAtNode(el)卸载该节点上的 React 树依赖数组为[]意味着createPortal的函数引用在整个组件生命周期内保持不变。这与仓库中 rendering.md 对 memoization 的论述一致useCallback专用于记忆化回调避免每次渲染都产生新引用从而防止不必要的子组件重渲染。3. 用useEffect()同步 DOM 挂载与卸载React.useEffect(() { if (el) portal.remove(); const newPortal createPortal(el); setPortal(newPortal); return () newPortal.remove(el); }, [el]);当el变化或首次挂载时若已有旧 Portal先调用portal.remove()清理旧节点再基于新的el调用createPortal(el)并setPortal(newPortal)更新状态cleanup 函数返回newPortal.remove(el)在el再次变化或组件卸载时卸载当前 Portal避免 DOM 节点泄漏。需要说明的是文档中的 cleanup 写作newPortal.remove(el)而remove的定义并不接收参数这里el参数实际上会被忽略纯属冗余传参按实现约定调用newPortal.remove()即可达到同等效果。4. 返回渲染器供 JSX 使用return portal.render;Hook 最终暴露的是portal.render即Portal组件。调用方可以像使用普通组件一样使用它例如示例中把它渲染进title元素——此时 Portalized Title 文本会脱离组件树结构直接出现在页面的title标签内部。四、使用方式与场景扩展示例中的用法非常直观const Portal usePortal(document.querySelector(title)); p Hello world! PortalPortalized Title/Portal /p虽然示例选择了title节点但usePortal可以传入任意已存在于页面中的 DOM 元素因此更典型的应用场景包括全局弹窗 / 遮罩层传入document.body让弹窗内容渲染到组件层级之外避免被祖先元素的overflow、z-index、transform等样式影响Tooltip / 下拉菜单渲染到 body 下规避父容器的overflow: hidden裁剪动态创建的容器先document.createElement(div)再传入实现完全的运行时挂载。注意传入的el必须是真实 DOM 节点且应在浏览器环境中存在在服务端渲染SSR场景下document不存在需要像仓库中的 useTitle Hook 那样先用typeof document ! undefined做环境守卫本文的实现本身并不包含这一步。五、测试 Portal仓库中的配套实践Portal 组件写好后如何测试仓库中的 testing-portals.md 给出了配套方案测试的核心只关心portalized 输出是否正确因此可以 mock 掉ReactDOM.createPortal让它在原地渲染子元素describe(MyComponent, () { beforeAll(() { ReactDOM.createPortal jest.fn((element, node) { return element; }); }); afterEach(() { ReactDOM.createPortal.mockClear(); }); it(should render correctly, () { const component renderer.create(MyComponentHello World!/MyComponent); expect(component.toJSON()).toMatchSnapshot(); }); });该文档同时指出这种 mock 的局限测试中的 DOM 与应用真实 DOM 不同可能降低测试的鲁棒性而portal 目标节点缺失这类与真实 DOM 相关的问题必须单独测试。这正是usePortal这类动态创建逻辑需要格外关注的边界情况。六、关键要点与注意事项依赖数组要完整useEffect的依赖为[el]若el来自 props 或 state需保证每次传入的是稳定引用否则会导致 Portal 被反复重建。cleanup 必须存在不清理旧 Portal组件卸载后会留下游离的 DOM 子树。createRoot与旧 API 的差异示例末尾使用ReactDOM.createRoot(...).render(...)这是 React 18 的并发渲染入口与仓库 React Hooks 集合 中为 React 18 提供可复用 Hooks的定位一致而ReactDOM.unmountComponentAtNode属于命令式卸载 API两者配套使用。挂载目标要有保证document.querySelector(title)这类查询如果返回nullcreatePortal(children, null)会抛出错误建议在实际项目中做空值校验。样式隔离Portal 将内容渲染到父组件树之外事件冒泡仍会沿 React 组件树传播React 18 中合成事件按真实 DOM 传播这是使用 Portal 时容易被忽视的行为差异。七、总结usePortal是一个精巧的 Hook 组合范例useState承载创建结果、useCallback保证工厂函数引用稳定、useEffect负责生命周期同步。它把 React Portal 从JSX 声明式用法升级为可编程、可复用、可清理的 Hook 能力是弹窗、提示层等脱离文档流的 UI 场景下的实用基础设施也是理解 React 组合式 API 如何协作的好案例。如需深入可继续阅读仓库内的 React Hooks 集合、Portal 测试实践 以及 React 渲染机制详解。赞分享教程文档【免费下载链接】30-seconds-of-codeCoding articles to level up your development skills项目地址https://gitcode.com/gh_mirrors/30/30-seconds-of-code点击查看免费下载相关推荐30-seconds-of-code React useTimeout Hook用自定义 Hook 声明式封装 setTimeout 定时器30 seconds of code React useTimeout Hook用自定义 Hook 声明式封装 setTimeout 定时器 setTimeo教程文档30 seconds of code 实战用 React useInterval 自定义 Hook 声明式封装 setInterval30 seconds of code 实战用 React useInterval 自定义 Hook 声明式封装 setInterval 定时器逻辑与 Reac教程文档RIOT 系统 ztimer_msg 测试应用深度解析从 xtimer 到 ztimer 的移植与时钟选择RIOT 系统 ztimer_msg 测试应用深度解析从 xtimer 到 ztimer 的移植与时钟选择 导读 本文围绕 RIOT 操作系统 tests/s教程文档上一篇YouMightNotNeedJS项目架构深度剖析Jade、Stylus和构建流程解析下一篇NDM (npm Desktop Manager) 安装与配置指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
延伸阅读

更多相关文章

2026/10/6 12:09:07

STM32引脚输入

文章目录 前言一、看原理图二、开始编程1.开启时钟2.配置GPIOA.0 上拉输入3.读取 GPIOA.0 引脚 GPIOA_IDR 0位上是1(按键松开),输入就是高电平,否则就是低电平(按键按下) 三、完整程序四 测试效果五、参考别…

2026/10/6 12:04:06

USB信号不稳定?共模电感选型是关键:从原理到实测

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/6 12:59:09

机器学习驱动的自动音乐生成优化:从符号建模到可控采样

简介:基于机器学习的自动音乐生成软件,核心采用长短期记忆网络模型,代替常见的简单循环神经网络与WaveNet方案,在最少人为干预下生成一段短曲并播放,缓解同质化问题。资源面向深度学习与音乐生成交叉方向的学习者&…

2026/10/6 12:59:09

AGV调度仿真平台实战:从A*路径规划到多车避让与死锁恢复

简介:这份AGV调度系统仿真平台资料包,面向物流、智能制造、人工智能及自动化方向的在校生与科研人员,可用于毕业设计、课程设计或项目初期原型验证。资源聚焦AGV任务调度与路径规划的可视化仿真,让使用者无需搭建实体设备即可观察…

2026/10/6 12:59:09

基于NUT的医院UPS实时监测系统实战解析

设备科半夜接到电话,CT室市电闪断,UPS顶上去了,可三分钟后电池电量掉到15%,还没等值班工程师赶到现场,设备已经因为电量耗尽强制关机。片子没出完,患者多等了两小时,科室主任的脸色比报告单还难…

2026/10/6 12:59:09

招生宣传管理系统毕设指南:从源码部署到答辩全流程

每年到了毕业季,计算机相关专业的学生就开始循环纠结同一件事:选题怎么定、系统怎么做、论文怎么写、答辩怎么过。你要是打开各类资源平台搜“招生宣传管理系统”,大概率会看到“源码 lw 部署文档 讲解”这种打包交付的毕设项目。今天我不…

2026/10/6 12:59:09

Claude Code 全局 Skill 配置指南:一次配置,所有项目可用

1. 这个配置思路到底解决什么问题先说个场景。我最初接触 Claude Code 时,Skill 功能刚出来不久,社区里的玩法五花八门,但大家基本都遵循一个习惯:每个项目目录下自己放一份.claude文件夹,把要用到的 Skill 塞进去。单…

2026/10/6 12:54:09

UE USTRUCT 转 JSON:反射序列化与 FJsonObjectConverter 完整指南

项目标题:将 USTRUCT 类型的实例对象,转换成对应的 JSON 字符串格式服务端要做一份配置下发接口,要求客户端把玩家当前状态打包成 JSON 字符串 POST 上去。我第一次图省事,用FString::Printf一段一段手工拼 JSON,十几个…

2026/10/5 6:32:56

Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化

1. 从“Jev”说起:为什么我要把Agent接进浏览器“Jev”这个词最近在圈子里出现的频率越来越高,很多人第一次听到会以为是某个新模型的名字,其实它更像是一种思路——把Jev模型的能力当作底座,通过Agent的方式去接管浏览器&#xf…

2026/10/6 4:01:51

多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系

1. 从"单兵作战"到"集群协同":多智能体编排到底在解决什么问题如果你最近在折腾 Agent 相关的东西,大概率会有一种感觉:单个 Agent 能做的事情,其实很快就摸到天花板了。你给它一个提示词,挂几个工…

2026/10/5 17:38:27

无源低通滤波器设计实战:从RC到LC,手把手教你避开那些坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/6 0:03:23

MR25H40CDF+STM32F031C6工业级高可靠数据存储方案

1. 项目概述:为什么在工业现场非得用 MR25H40CDF 配 STM32F031C6 做数据存储?在工厂产线的 PLC 控制柜里、在风电变流器的散热片背面、在矿井监测终端的金属外壳下,你经常能看到一块指甲盖大小的黑色芯片——它既不是 Flash,也不是…

2026/10/6 0:03:23

MRAM+STM32工业断电数据保全实战指南

1. 项目概述:为什么在工业现场非得用 MR25H40CDF 配 STM32F031C6 做数据存储?在工厂产线的PLC柜里、在野外无人值守的环境监测终端里、在高速运转的包装机控制板上,你经常能看到一块指甲盖大小的黑色芯片,旁边贴着“MR25H40CDF”丝…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑