TypeScriptのもとでuseRefを使うときに知るべきRefObjectとMutableRefObjectについて
React v19 RC時点で、 MutableRefObject
の廃止と RefObject
への一本化、デフォルトでmutableになることが案内されています。続報に注意してください。
TypeScript環境でのReactの useRef
は、初期値と型引数の与え方によって返り値の型が RefObject
と MutableRefObject
のどちらかになります。どういう使い方のときにどう書いてどちらを得るべきかを、 @types/react
の更新まわりの議論を追った結果を示します。
この記事は2021年5月現在、React 17.0.2が最新バージョンの時点で記述します。
参考にした情報
-
https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31065#issuecomment-446425911
-
RefObject
とMutableRefObject
が別である理由について
-
-
https://github.com/DefinitelyTyped/DefinitelyTyped/pull/38228#issuecomment-529749802
-
useRef
のオーバーロードとそれがどう効果を発揮するかについて
-
-
https://github.com/DefinitelyTyped/DefinitelyTyped/blob/813a8799e465a7d5f0d6776643f20f93681e85e4/types/react/index.d.ts#L1012-L1052
-
useRef
のオーバーロードの役割が同様に説明されている
-
-
https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31065#issuecomment-446660394
-
useRef
の呼び出し方の列挙と、特別に対処している場合とその理由について
-
現状:型定義の実装
useRef
の返り値の型には2通りがあり、初期値と型引数の与え方によってどちらになるかがかわります。
interface RefObject<T> {
readonly current: T | null;
}
interface MutableRefObject<T> {
current: T;
}
これらの変化は useRef
の次のようなオーバーロードによって表現されています:
/* snip */
function useRef<T>(initialValue: T): MutableRefObject<T>;
/* snip */
// convenience overload for refs given as a ref prop as they typically start with a null value
function useRef<T>(initialValue: T|null): RefObject<T>;
/* snip */
// convenience overload for potentially undefined initialValue / call with 0 arguments
// has a default to stop it from defaulting to {} instead
function useRef<T = undefined>(): MutableRefObject<T | undefined>;
背景:refの役割の変化
hooks以前のref
Ref は render メソッドで作成された DOM ノードもしくは React の要素にアクセスする方法を提供します。
refはhooks以前からDOMノードやReactの要素にアクセスする方法として提供されてきました。
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return (
<div>
<input type="text" ref={this.myRef} />
<button onClick={() => alert(this.myRef.current.value)}>show</button>
</div>
);
}
}
.current
の初期値は null
であり、Reactのランタイムもアンマウント時に .current
に null
を書き込みます。
[react] add forwardRef and createRef functions for React v16.3 by ferdaber · Pull Request #24624 · DefinitelyTyped/DefinitelyTyped で追加された型定義は、次のようになっていました:
interface RefObject<T> {
readonly current: T | null;
}
/* snip */
function createRef<T>(): RefObject<T>;
RefObject
の定義が現行のものと同じであることがわかります。 .current
に readonly
がついていることの経緯は少なくともPull Request上では語られておらず、hooksの対応時にいくつか議論の中で説明されるのみです。
実際の用例を思えば、 .current
を更新するのはReactのランタイムであり、ライブラリのユーザー側実装からは参照するだけになっていることは容易にわかります。
hooks以後のref
hooks以来、refの利用例は単純なDOMへの参照のみならず、クラスコンポーネントでインスタンス変数が用いられるようなシチュエーションも含むようになりました。
useRef() は ref 属性で使うだけではなく、より便利に使えます。これはクラスでインスタンス変数を使うのと同様にして、あらゆる書き換え可能な値を保持しておくのに便利です。
既存の RefObject
では新たな用例をサポートできません。DOMの参照をする利用例に対しての定義であって、インスタンス変数のようにユーザーが管理する値であることを前提にしていないからです。hooksのもとで広がった用例に対する表現をサポートするために、 readonly
を外す代わりに MutableRefObject
が設けられました。
ref. https://github.com/DefinitelyTyped/DefinitelyTyped/pull/30057#discussion_r230286351
useRef
のオーバーロードは、この分岐を引数・型引数の書き方で表現しています。
どうしたらいいか
インスタンス変数代わりに使うとき
概ね MutableRefObject
になるパターンがこちらにあたります。
// 推論されて、 `MutableRefObject<null>` になる。 `null` だけが入る
useRef(null);
// 推論されて、 `MutableRefObject<undefined>` になる。 `undefined` だけが入る
useRef(undefined);
// 推論されて、 `MutableRefObject<number>` になる。 `number` が入る
useRef(0);
// `MutableRefObject<number | null>` になる。 `number` と `null` が入る
useRef<number | null>(null);
// `MutableRefObject<React.CSSProperties>` になる
useRef<React.CSSProperties>({});
// `MutableRefObject<HTMLElement | null>` になる
useRef<HTMLElement | null>(null);
- 初期値を与えましょう
- 型引数は初期値からの推論に任せるか、明示的に書くかしましょう
DOMやコンポーネントにアクセスする手段として使うとき
function Comp() {
const ref = useRef<HTMLDivElement>(null);
return <div ref={ref}>foo</div>;
}
- 初期値に
null
を入れましょう - 型引数は中身に入りうるDOMノード、またはReactの要素の型だけを与えましょう
初期値が null
で型定義に null
を含まない場合を条件に、特別に RefObject
が得られるようになっています。これを守ることで、上記コード片での ref
は RefObject<HTMLDivElement>
になり、 .current
は readonly
になります。
useRef
をDOMにアクセスする手段に徹しさせるとき、一般的に .current
を更新するのはReactのランタイムだけで、ユーザーの実装から書き込むことは考えづらいです。 readonly
がつくことで、 .current
の値がReactによって管理されているものであり、ライブラリのユーザーは共有されているだけだと表現されているのです。うっかり .current
を書き換えようとしたときには、TypeScriptのレベルで、誤った使い方に気付くことができる、というわけです。
see. https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31065#issuecomment-446425911
まとめ
useRef
を、クラスコンポーネントにおけるインスタンス変数のように利用したいときは、 useRef
の型引数には実際に取りたい値の範囲を表明した型を与えるようにしましょう。
DOMノードやコンポーネントを参照する従来通りの用法のときは、 useRef<HTMLElement>(null)
のパターンで記述することで、気の利いた RefObject
型として得ることができます。
補遺
facebook/react での型定義
Reactの実装側にあるflowtypeによる useRef
の実装は次のようになっています:
export function useRef<T>(initialValue: T): {|current: T|} {
// snip
}
素朴ですね。オーバーロードもなく、特に特殊化も何もないようです。
Discussion
Types of property 'current' are incompatible.の謎が解けた