react-use 的 useEnsuredForwardedRef 与 ensuredForwardRef:安全使用 ForwardedRef 的完整指南
2026/9/19 7:43:57 网站建设 项目流程

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 提供useEnsuredForwardedRefensuredForwardRef所要解决的痛点:无论父组件是否传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} /> ); });

关键点在于:refundefined时,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; }

逐行拆解其机制:

  1. useRef(forwardedRef && forwardedRef.current)保证内部引用始终存在useRef的初始值只在首次渲染时生效。当forwardedRef存在时,取它当前的current值作为初始值;当forwardedRefundefined(父组件没传 ref)时,表达式短路求值得到undefineduseRef仍会返回一个合法的空 ref 对象。这一步从根源上杜绝了"拿到undefined"的可能。
  2. useEffect中回写forwardedRef.current = ensuredRef.current:组件挂载并完成 DOM 绑定后,ensuredRef.current指向真实的 DOM 节点;此时若外部确实传入了 ref,就把它同步回写,让父组件持有的 ref 也能实时看到子组件内部挂载的元素。这是"内外双向同步"的核心。
  3. 依赖数组[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+useEnsuredForwardedRefensuredForwardRef
组件内部 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是被引用的元素/实例类型(例如HTMLDivElementHTMLInputElement),P是组件 props 类型(默认{})。
  • ensuredForwardRef接收一个RefForwardingComponent<T, P>(即(props, ref) => ReactNode形式的渲染函数),返回标准的ForwardRefExoticComponent,因此它完全兼容React.forwardRef的返回类型,可以像普通 forwardRef 组件一样使用。
  • useEnsuredForwardedRef<T>接收并返回MutableRefObject<T>。值得注意的是,从类型签名可以推断,它面向的是对象形式的 refuseRef/useImperativeHandle场景),而非回调函数形式的 ref。

两个 API 均已从 src/index.ts 统一导出,即:

export { default as useEnsuredForwardedRef, ensuredForwardRef } from './useEnsuredForwardedRef';

因此在使用时直接从包入口导入即可:

import { useEnsuredForwardedRef, ensuredForwardRef } from 'react-use';

典型应用场景

useEnsuredForwardedRef/ensuredForwardRef适用于以下现实场景(也是 原文档 所述"从内部和外部同时使用 ref"诉求的落地):

  1. 测量组件内部 DOM 尺寸:在useEffect中调用getBoundingClientRect()offsetWidth等,配合useMeasure类逻辑做布局计算——但前提是 ref 必须有效。
  2. 聚焦与滚动控制:封装可复用的输入框、弹层组件时,父组件可选地传入 ref 以触发focus()/scrollIntoView(),子组件自身也需要在挂载后聚焦。
  3. 动画与第三方 DOM 库集成:需要把内部 DOM 节点交给动画库或图表库,同时父组件也持有同一节点时,保证任何一方都不会拿到空引用。
  4. 构建可复用的 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 的缺陷。

总体而言,useEnsuredForwardedRefensuredForwardRef是 react-use 中解决 ForwardedRef 空引用问题的标准工具:前者以 Hook 形式提供细粒度控制,后者以包装函数形式提供零成本接入,二者底层共用同一套"先自建保底 ref、再在 effect 中回写"的同步机制,并由 tests/useEnsuredForwardedRef.test.tsx 中的三个用例完整验证。

【免费下载链接】react-useReact Hooks — 👍项目地址: https://gitcode.com/gh_mirrors/re/react-use

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询