🔍

個人的にReact Hook Formで気をつけていること

2024/09/24に公開

はじめに

React Hook Formの特徴や個人的に気をつけていることをまとめました💫

React Hook Formの特徴

  • レンダリング回数を減らすことができる
  • バンドルサイズの軽減が可能

レンダリングについて

通常のReactでは、stateで値を管理し、stateを更新(setState)するたびに再レンダリングが発生します。

一方、React Hook Formでは、値の管理にstateではなくrefを使用するため、再レンダリングを抑制できます。
この仕組みにより、フォーム操作時に不要な再レンダリングを減らすことが可能です。
詳細は公式ドキュメントにも記載されています。

  • stateで値を管理する方法はControlled Componentsと呼ばれます。
  • 一方、refで値を管理する方法はUncontrolled Componentsと呼ばれます。
Type Controlled Components Uncontrolled Components
管理方法 state (React)での管理 ref (DOM)での管理
レンダリング 値が変化するたびに再レンダリングが発生する 値が変化しても再レンダリングが発生しない

個人的にReact Hook Formで気をつけているポイント

1. 値を取得する際のレンダリングコスト

簡単なフォーム(ログインフォームなど)であれば問題ないのですが、フォームの値を取得して何か処理をしたい場合には、少し複雑になります。

React Hook Formで値を取得する方法には、getValueswatchuseWatchの3種類があります。

種類 getValues useWatch watch
再レンダリング 発生しない コンポーネント単位で発生 フォーム全体で発生
使用タイミング 値を単に取得する場合 値の取得+UI/UX更新 値の取得+UI/UX更新
  • getValuesは再レンダリングを発生させないため、UI/UXの更新(たとえばエラーメッセージの表示など)には使用できません。
  • watchはフォーム全体に再レンダリングを発生させます。
  • 一方、useWatchはコンポーネント単位で再レンダリングを発生させるため、watchよりも影響範囲が狭いです。

しかし、useFormContextでコンポーネント化せずに、直接フォーム配下にuseWatchを記述してしまうと、watchと同様にフォーム全体を再レンダリングしてしまいます。

export default function App() {
  const { register, handleSubmit, control } = useForm({ mode: "onBlur" });
  const onSubmit = (data) => console.log(data);

  const useWatchValue = useWatch({ control, name: "useWatch" });

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {/* これだと意味がなく、フォーム全体が再レンダリングされてしまう */}
      <input {...register("useWatch")} />
      {useWatchValue}
      <button type="submit">Submit</button>
    </form>
  );
}

このように、React Hook Formの仕様を理解していないと、強みである「再レンダリングの抑制」の効果が薄れてしまいます。

2. Controllerの使用

外部コンポーネント(MUIなど)を使用する際、Controllerでコンポーネントをラップするのが一般的です。
ただ、ControllerでラップされたコンポーネントはControlled Components同様に再レンダリングが発生します。
本来、Uncontrolled Componentsの利点としてレンダリングの抑制を活かせるはずですが、Controllerを使うことでその効果が損なわれてしまいます。

では、なぜControllerでラップする必要があるのでしょうか。
これは、多くの外部コンポーネントがReactのrefを直接受け取る仕組みに対応していないためです。そのため、Controllerを利用して、フォームの制御を行う必要が出てきます。

React公式ドキュメントでは、refに関して次のように説明しています。

独自のコンポーネント、例えば <MyInput /> に ref を置こうとすると、デフォルトでは null が返されます。以下はそれを示す例です。ボタンをクリックしても入力フィールドにフォーカスが当たらないことに注意してください。

これは、デフォルトでは React は、コンポーネントが他のコンポーネントの DOM ノードにアクセスできないようにしているためです。自分自身の子でさえもです! これは意図的なものです。ただでさえ ref は控えめに使うべき避難ハッチ (escape hatch) です。別のコンポーネントの DOM ノードまで手動で操作できてしまうと、コードがさらに壊れやすくなってしまいます。

代わりに、内部の DOM ノードを意図的に公開したいコンポーネントは、そのことを明示的に許可する必要があります。コンポーネントは、自身が受け取った ref を子のいずれかに「転送 (forward)」するよう指定できます。MyInput が forwardRef API を使ってこれをどのように行うのか見てみましょう。

このように、独自のコンポーネントにrefを割り当てるにはforwardRefを使用する必要があると明記されています。
ただ、外部コンポーネントはforwardRefに対応していないことが多いので、Controllerでラップする必要があるということだと思います。
(MUIは割とforwardRefに対応してるコンポーネントが多いからControllerを使用しなくてもいけそう?)

まとめ

フォームが複雑になったり外部コンポーネントを使用したりする場合は、再レンダリングの影響を受けやすくなります。

利用する際には仕様をしっかり理解し、意図した通りにパフォーマンスを引き出せるようにしましょう💫

参考記事

https://react-hook-form.com/
https://scrapbox.io/mrsekut-p/react-hook-formでregisterとControllerのどちらを使うか

Discussion