开篇

日常开发中经常大量使用useRef hook,直到有一天被如下问题困住:

  1. const someRef = React.useRef<XType>(null);
  2. //...
  3. inputRef.current = 'some info' /* Cannot assign to 'current' because it is a read-only property */

报错意思很好理解,其实就是说此时我们的current是一个只读属性,那确实不能改写只读属性。但问题是为什么用useRef声明出来的变量是只读属性?
image.png

奇了怪了,来让我瞅瞅声明出来的ts定义是什么。
image.png

  1. //ts submittingRef: React.MutableRefObject<boolean>
  2. const submittingRef = useRef(false);
  3. //ts inputRef: React.RefObject<HTMLInputElement>
  4. const inputRef = React.useRef<HTMLInputElement>(null);

咦为什么ref会有两个定义?分别对应「RefObject」和「MutableRefObject」。

mutable这玩意儿不就是代表着可变的意思吗?让我分别康康对应的ts定义。

  1. interface MutableRefObject<T> {
  2. current: T;
  3. }
  4. interface RefObject<T> {
  5. readonly current: T | null;
  6. }

好家伙,两个人的区别就是多了个readonly,那为啥会有这个问题呢,或者说,难道根据我们不同的ref声明方式会返回不同的类型,有什么规则在里面呢?

image.png

正好搜到一篇老哥的文章,原文:Mutable and immutable useRef semantics with React & TypeScript

翻译一下好了,以下是翻译版本。

正文

在这篇文章中,你将会了解不同useRef hook的声明方式会如何影响到ref的current属性的不变性,我们将研究如何使current属性mutable和immutable,以及轻而易举的懂得分辨他们。

我们将要讨论的所有行为仅仅与Typescript相关,mutable / immutable的仅在编译时而不是运行时执行。

不变的 current 属性

一个不变的useRef hook语义上通常上来说会与DOM元素一起使用。举个简单的🌰:获取元素的ref并且在点击的时候聚焦该元素。

这里我是这样写的

  1. import * as React from "react";
  2. const Component = () => {
  3. const inputRef = React.useRef<HTMLInputElement>(null);
  4. return (
  5. <div>
  6. <input type="text" name="name" ref={inputRef} />
  7. <button type="button" onClick={() => inputRef.current?.focus()}>
  8. Click to focus the input
  9. </button>
  10. </div>
  11. );
  12. };

注意我在初始化useRef的时候的类型和值。我使用的语义标明我依赖React来帮我管理ref,在这个例子上而言,这代表了我不能去更改inputReg.current。即使我这么干了,Typescript一样会抱怨。

  1. import * as React from "react";
  2. const Component = () => {
  3. const inputRef = React.useRef<HTMLInputElement>(null);
  4. return (
  5. <div>
  6. {/* Cannot assign to 'current' because it is a read-only property */}
  7. <input type = "text" ref = {callbackRefValue => inputRef.current = callbackRefValue} />
  8. <button type="button" onClick={() => inputRef.current?.focus()}>
  9. Click to focus the input
  10. </button>
  11. </div>
  12. );
  13. };

在写了一段时间类似代码之后,我创建了一个经验法则来判断这个ref是不是immutable的。

当我们在初始化一个ref的时候,如果初始值是null并且初始值并不在对应的type里面,那么ref的current属性就是不immutable的。

在这个例子中,初始值null是不属于HTMLInputElement这个type里面,所以我们无法更改current属性。

可变的 current 属性

为了让我们的current属性mutable,我们需要改变一下我们的声明方式。

假设我们要写一个处理定时器的组件。useRef hook是一个理想的候选者,因为它可以很好地在定时器中维持一份引用。只要我们手上有定时器引用,那么我们可以确保在组件卸载的时候清除掉定时器。

还是一个🌰

  1. import * as React from "react";
  2. const Component = () => {
  3. const timerRef = React.useRef<number | null>(null);
  4. // This is also a valid declaration
  5. // const timerRef = React.useRef<number>()
  6. React.useEffect(() => {
  7. // Mutation of the `current` property
  8. timerRef.current = setTimeout(/* ... */)
  9. return clearInterval(timerRef.current)
  10. }, [])
  11. return (
  12. // ...
  13. );
  14. };

从一开始,我根本就不知道对后来声明setTimeout的引用是什么,所以我用了null来作为useRef的初始化值。除了类型之外,ref的声明可能看起来与之前的immutable的current属性部分中的声明十分相似。

然而,由于最初提供的初始值完全是属于我一开始在useRef中声明的type类型,在这里是number | null,所以现在current属性是允许mutable的。

和之前的immutablecurrent属性类似,这里也是我总结的一个经验。

如果useRef的初始化中,初始值是属于所提供的类型中的,那么他的current属性就是mutable的。

在这个例子,初始值null属于number | null,所以current属性是mutable的。

作为替代方案,我同样可以用以下方式声明timeRef变量

  1. const timerRef = React.useRef<number>(); // the `timerRef.current` is also mutable

为什么current属性在这里也是mutable的?因为这里timeRef是通过undefined隐式初始化的。该undefined值属于我声明的timeRef类型 - React.useRef根据初始值重载的类型

  1. const timerRef = React.useRef<number>();
  2. // Really is
  3. const timerRef = React.useRef<number>(undefined);
  4. // The `React.useRef` type definitions specify an overload whenever the type of the initial value is `undefined`
  5. function useRef<T = undefined>(): MutableRefObject<T | undefined>; // Notice the `MutableRefObject`.

总结

当我开始用React和Typescript干活的时候,ref的mutable和immutable他们之间的区别常常会困扰我,我希望这篇文章对你有所帮助以及能为你之前在项目中可能带有的某些疑惑扫清障碍。