React Hook Form でも再描画に気を付ける
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