大家好,我 ssh。
React的API大多设计的很优雅,比如经典的
this.setState
。
但有一个API从诞生之初就争议不断,甚至很多熟练的开发者都不知道这个API存在的意义。
在最新的React19中,官方团队甚至明确提出 —— 会弃用并移除这个API。
这可真是字面意义的
「始乱终弃」
。
他就是
forwardRef
。
他在什么背景下产生?为什么争议不断?最后为什么被弃用?本文就来聊聊
forwardRef
背后的故事。
forwardRef诞生的初衷
ref
是
reference
(引用)的缩写。既然是
引用
,那根据React元素类型的不同自然有不同的引用效果。
比如,传递给
React Element
的
ref
,可以获得对应原生元素实例的
引用
。
// domRef.current 对应 HTMLInputElement
传递给类组件的
ref
,可以获得类组件实例的
引用
。
// instanceRef.current 对应 SomeClassComponent实例
由于函数组件没有对应实例,所以函数组件没有什么可引用的。
从
「语义」
来讲,函数组件自然不能接收
ref prop
。
但是,虽然函数组件不能接收
ref prop
,但有时函数组件有
「转发ref」
的需要。
比如,下述代码中的函数组件
FC
有个子组件
input
。
input
作为
React Element
,有自己的实例。
function FC() {
return <input />;
}
如果
FC
的祖先元素想获得
input
实例的引用,就得让
FC
帮忙转发一下
下述代码中,
FC
经过
forwardRef
包裹,就能将子组件
input
的实例转发给
inputRef
了:
const FC = forwardRef((props, ref) => {
return <input ref={ref}/>;
});
<FC ref={inputRef}/>
forwardRef
中的
forward
从字面意义上讲就是
「转发」
的意思。
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}/>;
});
上面还只是小麻烦,更麻烦的是
forwardRef
与
TS
结合时造成的类型书写复杂。
如果你的函数组件接收范型类型,当该函数组件被
forwardRef
包裹,范型推导就会失效。
需要使用一些很
hack
的类型来断言
forwardRef
:
文章 TypeScript + React: Typing Generic forwardRefs 详细介绍了forwardRef + TS的各种坑
如果说以上坑都只是带来不便,那下面这个坑就是实打实的性能问题了。
由于
forwardRef
会在组件树中生成额外的层级,在应用比较庞大时可能产生性能问题。
比如,
React-Redux
的维护者
markerikson
在给
React-Redux
做性能基准测试时就发现,
forwardRef
会对页面
FPS
指标带来不利影响。
类似上面这些问题使
forwardRef
在社区的争议一直很大。
官方的态度
官方很早就意识到
forwardRef
的局限性,早在2019年,
sebmarkbage
就在
RFC #107
[1]
指出
「未来,最终会移除forwardRef」
。
Andrew
在23年也表达过
「forwardRef未来一定会被移除」
,之所以没有立刻做,是因为
「移除一个被大量使用的API属于Breaking Change」
。
按照
Semantic Versioning
(即形如
A.B.C
的版本号格式)的定义,
「不向下兼容的变化」
需要在主版本(即
A
)中体现。
所以
「移除forwardRef」
就和其他Breaking Change(比如移除
propTypes
和
defaultProps
)一起被包含在v19中。
促使官方最终决定移除
forwardRef
的,除了来自社区的各种抱怨,还有个重要的原因 ——
ref
最初的语义(对实例的引用)是在类组件盛行的时代产生的。随着
Hooks
的崛起,函数组件的重要性远超类组件。
既然类组件终将消失在历史中,那严守
ref
的语义就没意义了。
「不再执着于ref语义」
也带来了另一个源码层面的变化。
如果你细心观察会发现,对于
React Element
与类组件,传递进来的
ref
会被
「自动赋予对实例的引用」
,而传递给
forwardRef
的
ref
,需要开发者手动决定怎么用:
// domRef.current 自动获得 HTMLInputElement
// instanceRef.current 自动获得 SomeClassComponent实例
<SomeClassComponent ref={instanceRef} />
forwardRef((props, ref) => {
// 开发者自己决定将 ref.current 赋值给谁
// ...
});
这种区别导致在类组件中,如果用解构对象的方式操作
props
,很可能意外将
ref
传递下去并自动获得实例引用。
比如下面代码中,如果
otherProps
中包含
ref
,就会被传递给
SomeClassComponent
并获得
SomeClassComponent
实例的引用:
let {myCustomProp, ...otherProps} = props;
doSomething(myCustomProp);
return <SomeClassComponent {...otherProps} />;
所以,
ref
与
key
作为
JSX
中两个特殊字段会被特殊处理,而其他
props
会被直接透传。
鉴于:
-
-
-
未来
ref
也将变成一个
「可以被透传的普通prop」
。
总结
本文详细解释了
forwardRef
:
-
-
-
往深了说,
forwardRef
从出现到消失,反映的其实是一个权力问题 —— 类组件失势,导致失去对
ref
语义的定义权。