React Hook Formは非制御コンポーネントからどうやって変更を検知しているのか

こんにちは。エンジニアの根岸です。 コミューンには2年ほど前から副業で関わっていたのですが、昨年の12月に正社員としてジョインすることになりました。

コミューンのプロダクトには比較的長く関わっているのですが、知らないうちにフォームライブラリのReact Hook Formが導入されていました。 React Hook Formを使っていて、どうやって入力内容が変更されたことを検知しているのか疑問に思ったので調べてみました。

React Hook Formとは

React Hook Formは軽量かつ高パフォーマンスなフォームライブラリです。 ReactのフォームライブラリにはFormikやredux-fromなどがありますが、それらと比較して高速であることが公式サイトでアピールされています。 React Hook Formは後発のライブラリですが、最近はだいぶ名前を聞くようになりました。 いまフォームライブラリを採用するならFormikかReact Hook Formの2択になってきたように感じます。

ちなみに僕は1年半ほど前にReact Hook Formを業務で採用を試みたことがあります。 ですが当時のバージョンは拡張性が低く、実装の要件を満たさないことが発覚して、React Hook Formで書いたコードをすべてFormikに羽目になりました。 そのときはもう使わんと思ったのですが、今日ではv6までアップデートされて拡張性もかなり高くなりました。 いまのバージョンだったらFormikに書き直す必要もなかったでしょう。

非制御コンポーネントとはなにか

React Hook Formの最大の特徴は非制御コンポーネントを利用する点です。 非制御コンポーネントとはフォームデータをDOMによって管理するコンポーネントのことです。

React Hook Formを使ったフォームのコードを見ると、refにregister関数を渡していることがわかります。 フォームの値はinputタグのvalueに保持させて、React Hook Formはref経由で値を取得します。 React Hook Formはrefから値を取得することで、値が変化したことによるrerenderを防いでいます。

function App() {
  const { register, handleSubmit } = useForm();
  const onSubmit = data => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input name="example" ref={register} />
    </form>
  )
)

ここで僕が疑問に思ったのは変更の検知です。 他のフォームライブラリと同様に、React Hook Formでは値が入力されるたびにバリデーションのチェックができるのですが、どうやって実現しているのでしょうか?

React Hook Formのコードを読んでどうやってrefから変更の検知をしているか検証していきます。

registerからどうやって変更を検知するのか

React Hook FormのGitHubから実際のコードを見ていきます。

refに渡すregister関数について調べたいので、useFormのコードを調べます。 useFormのコードは/src/useForm.tsにあります。

useForm.tsの1042~1074行目にregister関数の実装が見つかります。 register関数の中では必ずregisterFieldRef関数が呼ばれています。 registerFieldRef関数にはregister関数が受け取ったrefを渡しているようです。

registerFieldRef関数の実装は908~1040行目にあります。 registerFieldRef関数が受け取ったrefは、変数fieldRefAndValidationOptionsのオブジェクトのプロパティとして渡されていました。(938~941行目)

さらに変数fieldRefAndValidationOptionsは変数Filedに渡されて(968~984行目)、refは最終的にattachEventListeners関数に渡されます。(1032~1039行目)

attachEventListeners関数の実装はsrc/logic/attachEventListeners.tsにあります。 attachEventListeners関数の中ではrefのaddEventListenerメソッドを呼び出して、第一引数にEVENTSという定数を渡しています。

EVENTSの定数の中身を調べると下のようになっていました。

以上からregister関数は受け取ったrefのaddEventListenerメソッドを呼び出してblur, change, inputのイベントリスナーを登録していることがわかりました。

したがってReact Hook FormではaddEventListenerを使って、フォームの変更を検知していると考えられます!謎が解けました!