📖

React Hook FormでuseFieldArrayの初期化方法を間違っていた話。初期値はdefaultValuesで設定すること

2023/02/08に公開

React Hook FormのuseFieldArrayは動的にフォームの入力項目を増減する必要がある場合にとても便利な機能です。

とても便利な機能なのですが、私は最初間違って使っていました。
正しい使い方を理解したので記事にまとめておこうと思います。

useFieldArrayの公式ドキュメントは下記を参照してください。

https://react-hook-form.com/api/usefieldarray/

どう間違えたのか?

サンプルコードを交えて説明した方が理解しやすいので、サンプルコードに沿って説明します。
サンプルコードのバージョンは下記の通りです。

  • react: 18.2.0
  • react-hook-form: 7.43.0

例えば下記のようなデータをAPIで受け取り、valueの数だけテキストボックスを作成する画面を考えます。

{
  hoge: [
    { value: 100 },
    { value: 0 },
    { value: 10 },
    { value: 50 }
  ]
}

上記を取得したら次の画面を表示します。
スクリーンショット 2023-02-05 22.58.24.png

間違った使い方

最初、replaceを使って下記のように実装しました。

import { useEffect } from 'react';
import { useFieldArray, useForm, } from 'react-hook-form';

export const Form = () => {
  const {
    control,
    register,
    formState: { isDirty },
  } = useForm<{ hoge: { value: number }[] }>();

  const { fields, replace } = useFieldArray({
    name: 'hoge',
    control,
  });

  useEffect(() => {
    // APIで取得した結果を基にreplaceでfieldsを置き換える
    // response = hoge: [
    //   { value: 100 },
    //   { value: 0 },
    //   { value: 10 },
    //   { value: 50 }
    // ]
    const response = hogeApi();
    replace(response.hoge);
  }, [replace]);

  return (
    <form>
      {fields.map((field, index) => (
        <div key={index}>
          <input {...register(`hoge.${index}.value`)} />
        </div>
      ))}
      <button disabled={!isDirty}>更新</button>
    </form>
  );
};

下記の画面が表示され、ぱっと見は正しく表示されているように見えますが、よく見るとisDirtyが機能していません。
スクリーンショット 2023-02-05 22.58.24.png

isDirtyは初期値はfalseで、値が編集されるとtrueになります。
<button disabled={!isDirty}>と実装しているので、isDirtyがfalseの場合はボタンが無効化されるはずなのですが、最初から有効になっています。
正しく動作していないので間違った使い方だったとわかります。

正しい使い方

defaultValuesを活用して下記のように実装しました。

// 呼び出し元でhogeApi()を実行し、レスポンスをpropsで渡す
export const Form = ({ hoge }: { hoge: { value: number }[] }) => {
  const defaultValues = { hoge };
  const {
    control,
    register,
    formState: { isDirty },
  } = useForm<{ hoge: { value: number }[] }>({
    defaultValues
  });

  const { fields } = useFieldArray({
    name: 'hoge',
    control,
  });

  return (
    <form>
      {fields.map((field, index) => (
        <div key={index}>
          <input {...register(`hoge.${index}.value`)} />
        </div>
      ))}
      <button disabled={!isDirty}>更新</button>
    </form>
  );
};

スクリーンショット 2023-02-05 23.25.10.png

今回の場合、isDirtyが想定通りに動作して初期表示ではボタンが無効化されており、値を変更するとボタンが有効になります。

※ 注意点
defaultValuesは一度設定すると変更できないため、最初の処理のようにuseEffectで取得した値で後から変更することはできません。
そのため、APIで取得する処理を外出しして、propsで渡すようにしました。

react-hook-formの初期値について

react-hook-formの初期値はuseFieldArrayに限らずdefaultValuesを使うのが正しいやり方のようです。
defaultValuesを使っていなくてもそれっぽく動作しますが、isDirtyなど細かなところで上手く動かないことがあります。
React Hook Formを使っているけど、defaultValuesを使っていない場合はぜひ見直してみてください。

Discussion