
react-use 的 useEnsuredForwardedRef 与 ensuredForwardRef安全使用 ForwardedRef 的完整指南【免费下载链接】react-useReact Hooks — 项目地址: https://gitcode.com/gh_mirrors/re/react-use本篇技术指南以 react-use 仓库中的 useEnsuredForwardedRef 文档 为骨架深入讲解useEnsuredForwardedRefHook 与ensuredForwardRef包装函数的使用方法、底层实现原理与测试验证。读完本文你将掌握在React.forwardRef场景下无论父组件是否传入 ref都能在子组件内部拿到有效 DOM 引用的两种标准姿势并能基于 源码 理解其内部同步机制。背景ForwardedRef 的困境在 React 中父组件往往需要通过ref访问子组件内部的 DOM 节点。标准做法是使用React.forwardRef将父组件的ref转发给子组件内部的元素const Child React.forwardRef((props, ref) { return div ref{ref} /; });这样做的前提是父组件确实传入了ref。然而在实际项目中forwardRef只能保证转发通道存在并不能保证父组件一定给了你一个 ref。当父组件没有传入ref时子组件渲染函数的第二个参数ref会是undefined——此时如果在子组件的生命周期 Hook如useEffect里直接访问ref.current就会抛错或拿到空值。正如 原文档 所描述的当你既需要在组件外部持有这个引用又需要在组件内部的生命周期里操作它时事情就变得复杂了——你无法保证父组件一定会传ref进来。这正是 react-use 提供useEnsuredForwardedRef与ensuredForwardRef所要解决的痛点无论父组件是否传ref子组件内部拿到的始终是一个有效、可用的引用。解法一useEnsuredForwardedRef Hook细粒度控制如果你希望保留React.forwardRef的完整写法只是在内部对ref做一层保底处理可以直接使用useEnsuredForwardedRefHook。这是 原文档 给出的 Alternative usageimport {useEnsuredForwardedRef} from react-use; const Demo () { return ( Child / ); }; const Child React.forwardRef((props, ref) { // 这里 ref 可能是 undefined父组件未传 ref 时 const ensuredForwardRef useEnsuredForwardedRef(ref); // ensuredForwardRef 永远是一个有效的引用 useEffect(() { console.log(ensuredForwardRef.current.getBoundingClientRect()) }, []) return ( div ref{ensuredForwardRef} / ); });关键点在于ref为undefined时useEnsuredForwardedRef依然会返回一个合法 ref 对象因此组件内部的useEffect可以放心访问ensuredForwardRef.current.getBoundingClientRect()而不会因为父组件偷懒没传 ref 而崩溃。实现原理查看 src/useEnsuredForwardedRef.ts其完整实现非常精简export default function useEnsuredForwardedRefT( forwardedRef: MutableRefObjectT ): MutableRefObjectT { const ensuredRef useRef(forwardedRef forwardedRef.current); useEffect(() { if (!forwardedRef) { return; } forwardedRef.current ensuredRef.current; }, [forwardedRef]); return ensuredRef; }逐行拆解其机制useRef(forwardedRef forwardedRef.current)保证内部引用始终存在useRef的初始值只在首次渲染时生效。当forwardedRef存在时取它当前的current值作为初始值当forwardedRef为undefined父组件没传 ref时表达式短路求值得到undefineduseRef仍会返回一个合法的空 ref 对象。这一步从根源上杜绝了拿到undefined的可能。useEffect中回写forwardedRef.current ensuredRef.current组件挂载并完成 DOM 绑定后ensuredRef.current指向真实的 DOM 节点此时若外部确实传入了 ref就把它同步回写让父组件持有的 ref 也能实时看到子组件内部挂载的元素。这是内外双向同步的核心。依赖数组[forwardedRef]同步动作只在forwardedRef引用发生变化时重新执行避免无谓的重复回写。测试如何验证保底行为tests/useEnsuredForwardedRef.test.tsx 中针对这一行为设计了三个用例与上面的实现一一对应外部已有 ref 时useEnsuredForwardedRef(ref)返回的对象与外部ref结构上严格相等toStrictEqual说明回写机制让两者最终指向同一个 DOM 节点测试第 19-37 行。forwardedRef 为undefined时传入undefined!依然能拿到有效引用且挂载div idtest_id ref{ref} /后ref.current.id test_id证明确保有效引用名副其实测试第 39-53 行。包装函数风格下父组件通过ref拿到子组件内部的 DOM 节点initialRef.current?.id test_id测试第 55-73 行。解法二ensuredForwardRef 高阶包装一行搞定如果不想在组件内部手动调用 Hookreact-use 还提供了ensuredForwardRef包装函数它直接取代React.forwardRef把确保引用有效的逻辑封装在内部。这是 原文档 给出的标准 Usageimport {ensuredForwardRef} from react-use; const Demo () { return ( Child / ); }; const Child ensuredForwardRef((props, ref) { useEffect(() { console.log(ref.current.getBoundingClientRect()) }, []) return ( div ref{ref} / ); });使用ensuredForwardRef包装后组件渲染函数收到的ref一定是有效引用可以直接在useEffect中调用ref.current.getBoundingClientRect()无需再关心父组件是否传了 ref。实现原理看 src/useEnsuredForwardedRef.ts 中ensuredForwardRef的实现export function ensuredForwardRefT, P {}( Component: RefForwardingComponentT, P ): ForwardRefExoticComponentPropsWithoutRefP RefAttributesT { return forwardRef((props: PropsWithChildrenP, ref) { const ensuredRef useEnsuredForwardedRef(ref as MutableRefObjectT); return Component(props, ensuredRef); }); }它的思路非常清晰在React.forwardRef内部调用useEnsuredForwardedRef处理外部传入的ref可能为undefined再把确保后的ensuredRef交给原始组件渲染函数。原始组件收到的永远是合法引用而外部父组件通过 ref 拿到的依然是对应的真实 DOM 节点。与直接使用 React.forwardRef 的对比维度React.forwardRefuseEnsuredForwardedRefensuredForwardRef组件内部 ref 有效性依赖你手动调用 Hook包装后天然保证侵入性组件内部需多写一行 Hook 调用替换forwardRef即可内部无需改动灵活性可对 ref 做额外处理后再绑定固定行为适合标准转发场景外部 ref 可用性双向同步父组件可访问内部 DOM同样支持父组件 ref 指向内部 DOM两种方式最终都依赖同一个useEnsuredForwardedRef内核区别只在于由谁调用。类型签名参考原文档 的 Reference 一节给出了两个公开 API 的完整类型签名ensuredForwardRefT, P {}(Component: RefForwardingComponentT, P): ForwardRefExoticComponentPropsWithoutRefP RefAttributesT; useEnsuredForwardedRefT(ref: React.MutableRefObjectT): React.MutableRefObjectT;结合 源码 可以做如下解读泛型T是被引用的元素/实例类型例如HTMLDivElement、HTMLInputElementP是组件 props 类型默认{}。ensuredForwardRef接收一个RefForwardingComponentT, P即(props, ref) ReactNode形式的渲染函数返回标准的ForwardRefExoticComponent因此它完全兼容React.forwardRef的返回类型可以像普通 forwardRef 组件一样使用。useEnsuredForwardedRefT接收并返回MutableRefObjectT。值得注意的是从类型签名可以推断它面向的是对象形式的 refuseRef/useImperativeHandle场景而非回调函数形式的 ref。两个 API 均已从 src/index.ts 统一导出即export { default as useEnsuredForwardedRef, ensuredForwardRef } from ./useEnsuredForwardedRef;因此在使用时直接从包入口导入即可import { useEnsuredForwardedRef, ensuredForwardRef } from react-use;典型应用场景useEnsuredForwardedRef/ensuredForwardRef适用于以下现实场景也是 原文档 所述从内部和外部同时使用 ref诉求的落地测量组件内部 DOM 尺寸在useEffect中调用getBoundingClientRect()、offsetWidth等配合useMeasure类逻辑做布局计算——但前提是 ref 必须有效。聚焦与滚动控制封装可复用的输入框、弹层组件时父组件可选地传入 ref 以触发focus()/scrollIntoView()子组件自身也需要在挂载后聚焦。动画与第三方 DOM 库集成需要把内部 DOM 节点交给动画库或图表库同时父组件也持有同一节点时保证任何一方都不会拿到空引用。构建可复用的 UI 基础组件组件作者无法预知使用方是否会传 ref用ensuredForwardRef包装后组件内部逻辑可无条件信任 ref 的有效性从而避免在代码里写一堆if (ref)判空。使用注意事项回调形式 ref 的边界由于签名限定为MutableRefObjectT若父组件传入回调函数形式的 refforwardedRef.current将访问不到——这是当前实现面向对象 ref 的设计前提使用时需与团队约定统一使用useRef形式的 ref。回写时机外部 ref 的current是在useEffect提交阶段之后才被同步的因此在同步发生之前外部访问ref.current可能仍是初始值组件内部请使用返回的ensuredRef作为唯一事实来源。依赖数组语义useEffect的依赖是[forwardedRef]即仅在 ref 对象引用变化时重新同步若父组件在渲染中反复创建新的 ref 对象会触发额外的同步但这属于 ref 本身的稳定性问题而非该 Hook 的缺陷。总体而言useEnsuredForwardedRef与ensuredForwardRef是 react-use 中解决 ForwardedRef 空引用问题的标准工具前者以 Hook 形式提供细粒度控制后者以包装函数形式提供零成本接入二者底层共用同一套先自建保底 ref、再在 effect 中回写的同步机制并由 tests/useEnsuredForwardedRef.test.tsx 中的三个用例完整验证。【免费下载链接】react-useReact Hooks — 项目地址: https://gitcode.com/gh_mirrors/re/react-use创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考