摘要
本文讲述了 React 中的 forwardRef API,它诞生于函数组件有转发 ref 需求的背景下,但由于存在如在 React Dev Tools 中看不到组件名、与 TS 结合类型书写复杂、产生性能问题等诸多问题而争议不断,官方也早有弃用之意,最终会在 React19 中被移除,这反映了类组件失势带来的 ref 语义定义权的转移
一、forwardRef 的产生背景
forwardRef 的出现是因为函数组件虽然本身不能接收 ref prop,但有时有转发 ref 的需求。例如,函数组件 FC 包含一个 input 子组件,FC 的祖先元素若想获取 input 实例的引用,FC 可通过 forwardRef 包裹来转发 input 的实例引用,如下面代码所示:
const FC = forwardRef((props, ref) => {
return <input ref={ref}/>;
});
<FC ref={inputRef}/>
二、forwardRef 存在的问题
(一)组件名显示问题
如果函数组件是匿名函数被 forwardRef 包裹时,在 React Dev Tools 中看不到组件名,需要写成具名函数组件才行,像这样:
// 匿名的函数组件(会导致看不到组件名)
const FC = forwardRef((props, ref) => {
return <input ref={ref}/>;
});
// 具名的函数组件(能看到组件名)
const FC = forwardRef(function FC(props, ref) {
return <input ref={ref}/>;
});
(二)与 TS 结合的类型问题
当函数组件接收范型类型且被 forwardRef 包裹时,范型推导会失效,需要使用一些特殊的类型断言方式,具体可参考文章TypeScript + React: Typing Generic forwardRefs
。
(三)性能问题
forwardRef 会在组件树中生成额外的层级,在大型应用中可能产生性能问题。例如 React - Redux 的维护者 markerikson 在做性能基准测试时发现它会对页面 FPS 指标产生不利影响。
三、官方对 forwardRef 的态度
官方很早就意识到 forwardRef 的局限性,2019 年 sebmarkbage 就在 RFC # 107 指出未来会移除它。Andrew 在 23 年也表达过相同看法,只是因为移除一个被大量使用的 API 属于 Breaking Change,所以一直未立即执行。按照 Semantic Versioning 的规定,不向下兼容的变化需在主版本中体现,所以移除 forwardRef 和其他 Breaking Change(如移除 propTypes 和 defaultProps)一起被包含在 v19 中。促使官方最终决定移除它的原因除了社区的抱怨,还因为 ref 的语义是在类组件盛行时代产生的,随着 Hooks 的崛起,函数组件更重要,类组件逐渐失势,ref 语义也不再那么重要。
四、ref 语义相关的源码层面变化
对于 React Element 与类组件,传递进来的 ref 会被自动赋予对实例的引用,而传递给 forwardRef 的 ref 需要开发者手动决定如何使用。例如:
// domRef.current 自动获得 HTMLInputElement
<input ref={domRef} />
// instanceRef.current 自动获得 SomeClassComponent 实例
<SomeClassComponent ref={instanceRef} />
forwardRef((props, ref) => {
// 开发者自己决定将 ref.current 赋值给谁
//...
});
这种区别导致在类组件中,如果用解构对象的方式操作 props,很可能意外将 ref 传递下去并自动获得实例引用。所以 ref 与 key 在 JSX 中会被特殊处理,其他 props 则直接透传。
五、总结
本文详细阐述了 forwardRef 产生的背景、作用,存在的问题以及 React 核心团队对此的思考。forwardRef 从出现到消失,反映了类组件失势从而失去对 ref 语义的定义权,进而导致 forwardRef 这个具体实现也不复存在。