📮

React Hook Form でも再描画に気を付ける

2022/08/28に公開

React Hook Form を使うと、useStateを使う制御フォームにありがちな「頻繁な再描画」を手短かに防ぐことができます。しかし、使い方によっては、その利点を崩してしまうことがあります。それが、useFormの戻り値に含まれるwatchの使用です。

watch は頻繁な再描画の原因になる

次のコンポーネントは、よくあるサインアップフォームです。emailに入力された文字数をカウントし、インタラクティブに「何文字入力されているか」を表示します。watchは、このような値の購読に利用できる API です。しかしコメントにあるとおり、emailに文字が入力されるたび、このフォーム全体が再描画されてしまします。これは、多くの要素を含むコンポーネントで避けたい実装例です。

const defaultValues: Form = {
  email: "",
  password: "",
};
export const MyForm = () => {
  const { register, watch } = useForm<Form>({ defaultValues });
  return (
    <div>
      <div>
        <input {...register("email")} type="email" />
        {/* watch() 起因で <MyForm> 全体が再描画されてしまう */}
        <span>{watch("email").length}</span>
      </div>
      <div>
        <input {...register("password")} type="password" />
      </div>
    </div>
  );
};

useWatch で購読コンポーネントを作る

回避策のひとつに、useWatch を使う方法があります。次の例のように、別コンポーネントとしてuseWatchを使用、入力されている文字列の長さを取得します。

type CounterProps = {
  name: keyof Form;
  control: Control<Form>;
  children: (count: number) => ReactNode;
};
function Counter({ name, control, children }: CounterProps) {
  const value = useWatch({ name, control });
  const count = value.length; // 文字入力のたび数が変わる
  return <>{children(count)}</>;
}

これを<MyForm>の子コンポーネントとして使用します。Functions as a Child パターンで実装しているのですが、さきほどと異なり、再描画される範囲が、必要な範囲だけに絞られるようになりました。

export const MyForm = () => {
  const { register, control } = useForm<Form>({ defaultValues });
  return (
    <div>
      <div>
        <input {...register("email")} type="email" />
        <Counter name="email" control={control}>
          {(count) => {
            // email 入力時、再描画されるのはここだけ
            return <span>{count}</span>;
          }}
        </Counter>
      </div>
      <div>
        <input {...register("password")} type="password" />
      </div>
    </div>
  );
};

メモ化して必要な時だけ再描画

Functions as a Child パターンで実装する時に便利なのが、再描画タイミングのコントロールです。zxcvbn というライブラリを使用して、パスワード入力欄に「パスワード強度チェック」機能を追加してみましょう。0〜4 レベルの変化があった時だけ、子コンポーネントが再描画されるように施します。

type PasswordScoreProps = {
  name: keyof Form;
  control: Control<Form>;
  children: (score: ZXCVBNScore) => ReactNode;
};
function PasswordScore({ name, control, children }: PasswordScoreProps) {
  const password = useWatch({ name, control });
  const score = zxcvbn(password).score; // 算出されたスコアを取得
  const childNode = useMemo(() => children(score), [children, score]);
  // メモ化し、スコア変更時にだけ、子コンポーネント再描画
  return <>{childNode}</>;
}

これで 「パスワード強度スコア判定が変わった時だけ」 再描画してくれるコンポーネントができました。watchを直接使うよりも一手間かかりますが、大きなフォームを実装する時などはuseWatchを使い、再描画範囲を分離すると良いでしょう。

export const MyForm = () => {
  const { register, control } = useForm<Form>({ defaultValues });
  return (
    <div>
      <div>
        <input {...register("email")} type="email" />
        <Counter name="email" control={control}>
          {(count) => {
            // email 入力時、再描画されるのはここだけ
            return <span>{count}</span>;
          }}
        </Counter>
      </div>
      <div>
        <input {...register("password")} type="password" />
        <PasswordScore name="password" control={control}>
          {(score) => {
            // 強度スコア変更時のみ、再描画される
            return <span>{score}</span>;
          }}
        </PasswordScore>
      </div>
    </div>
  );
};

Discussion