📑

TypeScriptのもとでuseRefを使うときに知るべきRefObjectとMutableRefObjectについて

2021/05/04に公開1

TypeScript環境でのReactの useRef は、初期値と型引数の与え方によって返り値の型が RefObjectMutableRefObject のどちらかになります。どういう使い方のときにどう書いてどちらを得るべきかを、 @types/react の更新まわりの議論を追った結果を示します。

この記事は2021年5月現在、React 17.0.2が最新バージョンの時点で記述します。

参考にした情報

現状:型定義の実装

useRef の返り値の型には2通りがあり、初期値と型引数の与え方によってどちらになるかがかわります。

interface RefObject<T> {
    readonly current: T | null;
}

ref. https://github.com/DefinitelyTyped/DefinitelyTyped/blob/813a8799e465a7d5f0d6776643f20f93681e85e4/types/react/index.d.ts#L84-L86

interface MutableRefObject<T> {
    current: T;
}

ref. https://github.com/DefinitelyTyped/DefinitelyTyped/blob/813a8799e465a7d5f0d6776643f20f93681e85e4/types/react/index.d.ts#L892-L894

これらの変化は 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. https://github.com/DefinitelyTyped/DefinitelyTyped/blob/813a8799e465a7d5f0d6776643f20f93681e85e4/types/react/index.d.ts#L1012-L1052

背景:refの役割の変化

hooks以前のref

Ref は render メソッドで作成された DOM ノードもしくは React の要素にアクセスする方法を提供します。

Ref と 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のランタイムもアンマウント時に .currentnull を書き込みます。

[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 の定義が現行のものと同じであることがわかります。 .currentreadonly がついていることの経緯は少なくともPull Request上では語られておらず、hooksの対応時にいくつか議論の中で説明されるのみです。

実際の用例を思えば、 .current を更新するのはReactのランタイムであり、ライブラリのユーザー側実装からは参照するだけになっていることは容易にわかります。

hooks以後のref

hooks以来、refの利用例は単純なDOMへの参照のみならず、クラスコンポーネントでインスタンス変数が用いられるようなシチュエーションも含むようになりました。

useRef() は ref 属性で使うだけではなく、より便利に使えます。これはクラスでインスタンス変数を使うのと同様にして、あらゆる書き換え可能な値を保持しておくのに便利です。

フック API リファレンス – React

既存の 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 が得られるようになっています。これを守ることで、上記コード片での refRefObject<HTMLDivElement> になり、 .currentreadonly になります。

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
}

ref. https://github.com/facebook/react/blob/b522638b994abfea16b981efdf88e11d1078c982/packages/react/src/ReactHooks.js#L90-L93

素朴ですね。オーバーロードもなく、特に特殊化も何もないようです。

Discussion