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 usage:
import {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 useEnsuredForwardedRef<T>( forwardedRef: MutableRefObject<T> ): MutableRefObject<T> { 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)时,表达式短路求值得到undefined,useRef仍会返回一个合法的空 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 id="test_id" ref={ref} />后ref.current.id === 'test_id',证明"确保有效引用"名副其实(测试第 39-53 行)。 - 包装函数风格下:父组件通过
ref拿到子组件内部的 DOM 节点(initialRef.current?.id === 'test_id')(测试第 55-73 行)。
解法二:ensuredForwardRef 高阶包装(一行搞定)
如果不想在组件内部手动调用 Hook,react-use 还提供了ensuredForwardRef包装函数,它直接取代React.forwardRef,把"确保引用有效"的逻辑封装在内部。这是 原文档 给出的标准 Usage:
import {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 ensuredForwardRef<T, P = {}>( Component: RefForwardingComponent<T, P> ): ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>> { return forwardRef((props: PropsWithChildren<P>, ref) => { const ensuredRef = useEnsuredForwardedRef(ref as MutableRefObject<T>); return Component(props, ensuredRef); }); }它的思路非常清晰:在React.forwardRef内部调用useEnsuredForwardedRef处理外部传入的ref(可能为undefined),再把确保后的ensuredRef交给原始组件渲染函数。原始组件收到的永远是合法引用,而外部父组件通过 ref 拿到的依然是对应的真实 DOM 节点。
与直接使用 React.forwardRef 的对比
| 维度 | React.forwardRef+useEnsuredForwardedRef | ensuredForwardRef |
|---|---|---|
| 组件内部 ref 有效性 | 依赖你手动调用 Hook | 包装后天然保证 |
| 侵入性 | 组件内部需多写一行 Hook 调用 | 替换forwardRef即可,内部无需改动 |
| 灵活性 | 可对 ref 做额外处理后再绑定 | 固定行为,适合标准转发场景 |
| 外部 ref 可用性 | 双向同步,父组件可访问内部 DOM | 同样支持,父组件 ref 指向内部 DOM |
两种方式最终都依赖同一个useEnsuredForwardedRef内核,区别只在于"由谁调用"。
类型签名参考
原文档 的 Reference 一节给出了两个公开 API 的完整类型签名:
ensuredForwardRef<T, P = {}>(Component: RefForwardingComponent<T, P>): ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>>; useEnsuredForwardedRef<T>(ref: React.MutableRefObject<T>): React.MutableRefObject<T>;结合 源码 可以做如下解读:
- 泛型
T是被引用的元素/实例类型(例如HTMLDivElement、HTMLInputElement),P是组件 props 类型(默认{})。 ensuredForwardRef接收一个RefForwardingComponent<T, P>(即(props, ref) => ReactNode形式的渲染函数),返回标准的ForwardRefExoticComponent,因此它完全兼容React.forwardRef的返回类型,可以像普通 forwardRef 组件一样使用。useEnsuredForwardedRef<T>接收并返回MutableRefObject<T>。值得注意的是,从类型签名可以推断,它面向的是对象形式的 ref(useRef/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 的边界:由于签名限定为
MutableRefObject<T>,若父组件传入回调函数形式的 ref,forwardedRef.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),仅供参考