😅

React Hook時代におけるrefのユースケースとforwardRefについて

2022/07/14に公開

背景

筆者はVue2からReact17へのフレームワーク移行を進めていました。
React Hookによる関数コンポーネントへの移行を進めるにあたり、refをどう使うべきかとforwardRefを使うべきかどうかについて移行を進めながら考え、自分なりの結論を出しましたので紹介します。
時間がない方は最後のまとめだけ読んでください。

VueとReactにおける様々なref

VueやReactを扱っていると微妙に異なるニュアンスでrefというタームが使われています。
ここでは用語整理も兼ねて4つぐらいあるそれぞれのrefを紹介します。

Vue(Composition API)のref

これはReactで言うところのuseStateとほぼ等価でリアクティブステートを宣言するのに用います。

Vueの(Template) ref

これはDOMやVueコンポーネントを指すことができる(ポインタのような)インターフェースで、DOMの制御をしたり別のVueコンポーネントのメソッドなどを呼び出すことができます。refという名前のHTML属性を用いて、参照を変数に代入することでDOMを制御できるようになります。

Reactのref

公式ドキュメントは割と抽象的な表現をしているので分かりにくいですが、下記のStackOverFlowの回答を見てcreateRefの定義を見たことでスッキリ理解できました。currentをキーとして持つオブジェクトで、任意のオブジェクトを指せる可変参照(ポインタ)のようなものだと理解すれば良いです。refという名前の通りですね。
https://stackoverflow.com/questions/54620698/whats-the-difference-between-useref-and-createref
https://github.com/facebook/react/blob/b87aabd/packages/react/src/ReactCreateRef.js#L12-L20

Reactの(template) ref

Reactの公式ドキュメントにはtemplate refというタームは使われていないですが、区別したほうが理解が捗るためこの記事ではVueのタームを借用しています。
基本的にはVueのtemplate refと差はないですが、React Hook時代によく用いる関数コンポーネントには使用できず、工夫しなければ関数コンポーネント内のメソッド呼出もできません。
https://ja.reactjs.org/docs/refs-and-the-dom.html

関数コンポーネント (function components) には ref 属性を使用してはいけません。なぜなら、関数コンポーネントはインスタンスを持たないからです

refのユースケース

refは基本的に使用するべきではないと公式ドキュメントで言及されています。
https://ja.reactjs.org/docs/refs-and-the-dom.html#when-to-use-refs

宣言的に行えるものには ref を使用しないでください。

refを多用するということは、Reactにより実現されるDOM=f(data)というデータ駆動の宣言的レンダリングを阻害することに他ならないので、同意します。

一方で筆者はrefが仕方なく必要だと感じたケースがいくつかあります。

  • Pure JavaScriptで実装されたサードパーティライブラリとの連動
  • 関数コンポーネント内メソッドを上位コンポーネントから呼び出したい(≒リアクティブステートを再利用したい)
    • Vueの場合はコンポーネントメソッドがデフォルトで公開されているのに対して、Reactの関数コンポーネントはメソッドを隠蔽するので、ギャップを埋めるために使いました
    • createRefで上位コンポーネントからrefを渡し、レンダリングの度にメソッド参照を書き換えることでメソッドを公開できます

その他

  • Reactからでは制御できないDOM挙動の実現

などもrefを使う理由にはなりそうですが、筆者はまだその局面に直面したことはないです。

forwardRef

https://ja.reactjs.org/docs/refs-and-the-dom.html#refs-and-function-components

関数コンポーネントに対して ref が使用できるようにしたい場合は、forwardRef を(必要に応じて useImperativeHandle と組み合わせて)利用するか、コンポーネントをクラスに書き換えます。

という記述が公式ドキュメントにあるので、どうにかして使った方がいいんじゃないかと思ったのですが、関数コンポーネントで折角コンポーネントのインタフェースを揃えてるのに、あえてforwardRefというインタフェースでラップする必要が本当にあるのかと思っていました。

forwardRefは使わない

改めてドキュメントやrefの定義を見直して考えた結果、forwardRefは、コンポーネント内にあるDOM要素をrefで指せるようにバイパスしているだけだという結論に至りました。それであれば

  • refをpropsとして受け渡しするのと大差ない
  • propsであれば、refに対する適切な名前を定義することができる
    • 内側のDOM要素をrefというインタフェースで指せることに嬉しさがない
    • むしろコンポーネント定義の曖昧さを増やす要因にしかならない
  • forwardRefだと受け渡し可能なrefの数が1に限定される
    • propsとしてやりとりした方がrefの数に制約がなく一貫している
      • refの一方はforwardRef、もう一方はpropsなんて歪なインタフェースを見たくない

「forwardRef vs ref by props」みたいな単語でググったら下記の議論を発見でき、現実的な選択肢であることもわかりました。
https://stackoverflow.com/a/60237948/5225993

まとめ

  • refにはいくつか異なる概念が混ざっているので分別すると良い
    • リアクティブステートとしてのref(Vueのみ)
    • 可変参照としてのref(名前の通り)
    • JSXやVueテンプレートにおけるrefというHTML属性(これは可変参照としてのrefがテンプレートを指すための専用インタフェースだ)
      • template refあるいはref属性という風に区別した方が理解しやすい
  • refは使わなくて良いならば使わない
    • 一方で現実的にはrefが必要なシーンに直面する
  • コンポーネントメソッドの公開状況を見るとVueとReactの設計スタンスの差が垣間見える
    • Vueのインタフェースは公開的、Reactのインタフェースは隠蔽的
    • 筆者は関数型教徒なのでReactの方がやや馴染む
  • forwardRefは必要性が極めて低いAPIであると筆者は考えている
    • ref by propsの方が一貫性があり優れているから
    • 公式はforwardRefへの移行を推奨しているので意見が相違している
    • forwardRefを採用すべき、設計上、パフォーマンス上の理由をご存知の方はコメントなどで教えていただけると幸いです。
GitHubで編集を提案

Discussion