← プログラミングページの先頭へ

React Hook Form: Reactフォーム状態管理

React Hook Form (RHF) は、React エコシステムで一般的に使用されるフォーム管理ライブラリです。

主に以下を担当します。

  • フォームフィールドを登録および管理します。
  • ユーザー入力を収集します。
  • フィールドの検証とエラー管理。
  • フォームの送信。
  • Dirty、Touched、Submitting およびその他の状態を管理します。
  • 不必要な React の再レンダリングを最小限に抑えます。

React Hook Form は主に クライアント フォーム ステータス を管理し、TanStack Query は主に サーバー ステータス を管理します。

一般的なワークフロー:

Django API
    ↓
TanStack Query
    ↓
Server Data
    ↓
React Hook Form
    │
    ├── Field Registration
    ├── Form Values
    ├── Validation
    ├── Error Management
    └── Form State
    ↓
handleSubmit()
    ↓
TanStack Query Mutation
    ↓
Axios
    ↓
Django API

1. 基本的な使い方

インストール:

pnpm add react-hook-form

基本的な例:

import { useForm } from "react-hook-form";

type FormData = {
  username: string;
  email: string;
};

function ProfileForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<FormData>({
    defaultValues: {
      username: "",
      email: "",
    },
  });

  const onSubmit = (data: FormData) => {
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        {...register("username", {
          required: "Username is required",
        })}
      />

      {errors.username && <p>{errors.username.message}</p>}

      <input
        {...register("email", {
          required: "Email is required",
        })}
      />

      <button type="submit" disabled={isSubmitting}>
        Submit
      </button>
    </form>
  );
}

従来の React フォームと比較して、RHF では入力フィールドごとに個別の useState を作成する必要がありません。

2. useForm: フォームコントローラー

useForm() は、RHF のコアとなる Hook です。

const form = useForm<FormData>({
  defaultValues: {
    username: "",
    email: "",
  },
  mode: "onSubmit",
});

これは、一連のフォーム管理メソッドを作成して返します。

const {
  register,
  control,
  handleSubmit,
  watch,
  getValues,
  setValue,
  reset,
  trigger,
  setError,
  clearErrors,
  formState,
} = useForm<FormData>();

useForm 共通構成

パラメータ機能
defaultValuesフォームの値を初期化する
values外部データに基づいてフォーム値をリアクティブに更新します
mode初回検証用トリガーモード
reValidateMode送信後のエラーフィールドの再検証モード
resolver外部検証ライブラリにアクセスする
criteriaModeフィールドごとに収集される検証エラーの数を制御します
shouldFocusError検証が失敗したときにエラーフィールドに焦点を当てるかどうか
shouldUnregisterフィールドのアンロード時に登録をキャンセルするかどうか
disabledフォーム全体を無効にする

Validation Mode

Mode検証タイミング
onSubmit送信時のデフォルトは、
onBlurフィールドがフォーカスを失ったとき
onChange入力が変化したとき
onTouched最初の Blur の後、次の Change
allBlur、Changeの場合

たとえば:

useForm<FormData>({
  mode: "onBlur",
  reValidateMode: "onChange",
});

これは、最初の検証は主に Blur でトリガーされ、送信後にエラー フィールドがあった場合は Change で再検証されることを意味します。

onChange モードは頻繁に検証を実行する可能性があるため、複雑なフォームではパフォーマンスを考慮する必要があります。

3. register: 登録フォームフィールド

register() は、ネイティブ入力要素を RHF に登録します。

<input {...register("username")} />

以下とほぼ同等:

<input
  name="username"
  onChange={...}
  onBlur={...}
  ref={...}
/>

register() は主に以下を返します。

  • name: フィールド名。
  • onChange:フィールド変更処理関数。
  • onBlur: ロストフォーカス処理機能。
  • ref: 入力要素の参照。

従来の制御コンポーネントとの違い

従来の React:

const [username, setUsername] = useState("");

<input
  value={username}
  onChange={(event) => setUsername(event.target.value)}
/>;

RHF:

<input {...register("username")} />

RHF は通常、ネイティブ入力に対して制御されていない管理方法を採用しており、入力のたびにコンポーネント自身の setState を通じてフィールド値を更新する必要はありません。

これは、不必要なコンポーネントの再レンダリングを減らすのに役立ちます。

ネストされたオブジェクト

register("address.city");
register("address.country");

以下に対応します:

{
  address: {
    city: "Tokyo",
    country: "Japan"
  }
}

配列フィールド

register("users.0.name");

以下に対応します:

{
  users: [{ name: "Alice" }];
}

入力タイプの変換

HTML Input の値は通常、文字列です。

次のものが使用できます。

<input
  type="number"
  {...register("age", {
    valueAsNumber: true,
  })}
/>

または:

<input
  {...register("price", {
    setValueAs: (value) => Number(value),
  })}
/>

フォームの送信後にすべての数値フィールドを手動で変換することは避けてください。

4. handleSubmit: フォームの送信

handleSubmit() は、送信前の検証と、対応するコールバックの呼び出しを担当します。

Browser Submit
      ↓
handleSubmit
      ↓
Validation
      │
      ├── Success → onValid(data)
      │
      └── Failure → onInvalid(errors)

基本的な使い方

const onSubmit = (data: FormData) => {
  console.log(data);
};

<form onSubmit={handleSubmit(onSubmit)}>

handleSubmit(onSubmit) フォーム送信ハンドラーとして使用できる関数を返します。

成功と失敗のコールバック

import type {
  SubmitHandler,
  SubmitErrorHandler,
} from "react-hook-form";

const onValid: SubmitHandler<FormData> = (data) => {
  console.log(data);
};

const onInvalid: SubmitErrorHandler<FormData> = (errors) => {
  console.log(errors);
};

<form onSubmit={handleSubmit(onValid, onInvalid)}>

非同期送信

const onSubmit = async (data: FormData) => {
  try {
    await updateProfile(data);
  } catch (error) {
    console.error(error);
  }
};

非同期送信の場合、handleSubmit はコールバックによって返される Promise を待ちます。

RHF は、送信ハンドラーによってスローされた例外を自動的には飲み込まないため、独自に処理する必要があります。

5. Validation: フィールドの検証

最も単純な検証は register() に直接記述することができます。

<input
  {...register("username", {
    required: "Username is required",
    minLength: {
      value: 3,
      message: "At least 3 characters",
    },
    maxLength: {
      value: 20,
      message: "At most 20 characters",
    },
  })}
/>

共通の検証ルール

ルール機能
required必須
minLength最小の長さ
maxLength最大長さ
min最小値
max最大値
pattern正規表現
validateカスタム検証関数

たとえば:

register("age", {
  valueAsNumber: true,
  validate: (value) => value >= 18 || "Must be at least 18",
});

カスタムの非同期検証

register("username", {
  validate: async (value) => {
    const available = await checkUsername(value);

    return available || "Username already exists";
  },
});

このタイプの検証では、入力ごとにサーバーが呼び出されるのを避けるために、手振れ防止や検証トリガー時間の調整が必要になる場合があります。

同時に、フロントエンド検証は Django Serializer のサーバー側検証を置き換えることはできません。

6. Resolver: 外部検証ライブラリ

Resolver は、RHF と外部 Schema Validation Library の間のアダプターです。

一般的な検証ライブラリには次のものがあります。

  • Zod
  • Yup
  • Valibot
  • Joi

Zod をインストールします。

pnpm add zod @hookform/resolvers

例:

import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";

const schema = z.object({
  username: z.string().min(3).max(20),
  email: z.email(),
});

type FormData = z.infer<typeof schema>;

const form = useForm<FormData>({
  resolver: zodResolver(schema),
});

resolver は、外部検証結果を RHF が理解できるフィールド エラーに変換する責任があります。

Zodを使用する必要がありますか?

必須ではありません。

単純なフォームは直接使用できます。

register("username", {
  required: true,
  minLength: 3,
});

クロスフィールド検証、複雑なオブジェクト検証、または Schema の再利用が必要な場合は、Zod を検討してください。

プロジェクトがすでに OpenAPI + Orval を通じて TypeScript タイプを生成している場合は、すべての API Schema の定義を機械的に繰り返すことなく、実際のニーズに応じてクライアント ランタイム検証を追加するかどうかを選択できます。

7. formState: フォームのステータス

formState は、RHF の非常に重要な状態のセットです。

const {
  formState: {
    errors,
    isDirty,
    dirtyFields,
    touchedFields,
    isValid,
    isValidating,
    isSubmitting,
    isSubmitted,
    isSubmitSuccessful,
    submitCount,
  },
} = useForm<FormData>();
ステータス意味
errorsフィールド検証エラー
isDirty現在のフォーム全体の値がデフォルト値と異なるかどうか
dirtyFieldsDirty 各フィールドのステータス
touchedFieldsユーザーがタッチしたフィールド
isValidフォーム検証結果は有効ですか?
isValidating検証は進行中ですか?
isSubmittingコミットコールバックが実行されているかどうか
isSubmitted提出されましたか?
isSubmitSuccessful最新の送信が正常に完了したかどうか
submitCount提出数

isDirty および dirtyFields

仮定:

defaultValues: {
  username: "Alice",
  email: "alice@example.com",
}

ユーザーはユーザー名を Bob に変更します。

isDirty = true

dirtyFields = {
  username: true
}

ユーザーがユーザー名を Alice に戻す場合:

isDirty = false

フォーム全体の現在の値が再び defaultValues と一致するためです。

ただし、dirtyFields にはフィールドレベルの状態セマンティクスがあり、常に isDirty と完全に同期していると見なすべきではありません。

すべてのフィールドに対して完全で安定した defaultValues を提供することをお勧めします。そうしないと、Dirty の判定が期待どおりにならない可能性があります。

touchedFields

フィールドがフォーカスを取得した後の Blur:

Focus → Blur
      ↓
touchedFields.username = true

フィールド値を変更する必要はありません。

isSubmitting

<button type="submit" disabled={isSubmitting}>
  {isSubmitting ? "Submitting..." : "Submit"}
</button>

注: isSubmitting は、handleSubmit の送信プロセスを追跡します。

onSubmit が単に呼び出した場合:

mutation.mutate(data);

その後すぐに戻ります。通常、isSubmitting は Mutation が完了するまで継続的に待機しません。

使用する必要があります:

const onSubmit = async (data: FormData) => {
  await mutation.mutateAsync(data);
};

または、TanStack Query の mutation.isPending 管理サーバーを直接使用してステータスを送信します。

8. watch: サブスクリプションフォームの値

watch() はフォーム値を取得するために使用され、フィールドが変更されると UI が更新されます。

たとえば:

const country = watch("country");

return (
  <>
    <select {...register("country")}>
      <option value="JP">Japan</option>
      <option value="US">United States</option>
    </select>

    {country === "JP" && <input {...register("prefecture")} />}
  </>
);

country が変更されると、コンポーネントは新しい値に基づいてさまざまなフィールドを表示できます。

一般的な呼び出し方法

watch(); // フォーム全体を聞く
watch("username"); // 単一フィールドを監視する
watch(["city", "country"]); // 複数のフィールドを監視する

watch() では、関連付けられた値が変更されたときに、それを使用するコンポーネントが再レンダリングされる可能性があります。

複雑なフォームの場合は、トップレベルのすべてのフィールドを無差別に監視しないことをお勧めします。

9. useWatch: 部分的なサブスクリプション

useWatch() は watch() に似ていますが、それを使用するコンポーネントへの再レンダリングの効果が制限されます。

import { useWatch } from "react-hook-form";

function CountryDetails({ control }: { control: Control<FormData> }) {
  const country = useWatch({
    control,
    name: "country",
  });

  return <p>Selected country: {country}</p>;
}

watch および useWatch

特長watchuseWatch
主な機能フォーム値を取得してサブスクライブするフォーム値を取得してサブスクライブする
使用法useForm によって返されるメソッド独立した Hook
更新範囲フォームのルートコンポーネントに影響を与える可能性がありますサブコンポーネントでの分離が容易になる
該当するシナリオシンプルな形複雑なフォーム、部分的なサブスクリプション

たとえば、数十のフィールドを含むフォームで、特定のサブコンポーネントのみが country をリッスンする必要がある場合、通常はサブコンポーネント内で useWatch() を使用することをお勧めします。

10. getValues: 現在値を読み取ります。

getValues() は現在のフィールド値を直接読み取ることができますが、サブスクリプションは確立しません。

const username = getValues("username");

イベント処理関数で一般的に使用されます。

const handleSomething = () => {
  const username = getValues("username");

  console.log(username);
};

watch および getValues

方法購読するかどうか目的
watchはい値が変更された場合は UI を更新します
useWatchはいローカルコンポーネントのアップデート
getValuesいいえ現在値を一時的に読み出します

ボタンをクリックしたときに値を 1 回だけ読み取る場合は、通常は getValues() を使用します。

11. setValue: フィールドをアクティブに変更する

setValue() は、コードを通じてフォーム値を変更するために使用されます。

setValue("username", "Bob");

ステータス更新をアクティブにトリガーすることもできます。

setValue("username", "Bob", {
  shouldDirty: true,
  shouldValidate: true,
  shouldTouch: true,
});
パラメータ機能
shouldDirtyDirtyステータスを更新
shouldValidate検証を実行する
shouldTouchTouchedステータスを更新

例

ユーザーが都市を選択すると、国が自動的に入力されます。

setValue("country", "Japan", {
  shouldDirty: true,
});

注: setValue() は、React の setState() と同じではありません。

RHF は、サブスクリプション、エラー、状態の変化に基づいてどのコンポーネントを更新する必要があるかを決定します。通常は毎回フォーム全体を再レンダリングする必要はありません。

12. reset: リセットフォーム

reset() は、フォームの値と関連ステータスをリセットするために使用されます。

reset();

通常は現在の defaultValues に復元されます。

以下のことも可能です:

reset({
  username: "Alice",
  email: "alice@example.com",
});

デフォルト構成では、指定された新しい値が新しいデフォルト値のベースラインになります。

したがって:

Current Values = New Values
Default Values = New Values
isDirty = false

ユーザー情報を非同期で取得する

const { data: user } = useGetUser();

const form = useForm<FormData>({
  defaultValues: {
    username: "",
    email: "",
  },
});

useEffect(() => {
  if (user) {
    form.reset({
      username: user.username,
      email: user.email,
    });
  }
}, [user, form.reset]);

ユーザーが編集中のデータを保護する

サーバーがデータを再取得したが、ユーザーが一部のフィールドを変更した場合は、次の点を考慮してください。

reset(newData, {
  keepDirtyValues: true,
});

このようにして、Dirty フィールドはユーザーの現在の入力を保持し、他のフィールドは新しいデータに基づいて更新されます。

これは、背景 Refetch と TanStack Query を組み合わせた場合に特に重要です。

そうしないと、Query データが更新されるたびに無条件に reset(data) が実行されると、保存されていないユーザーの入力が上書きされる可能性があります。

13. values: 応答性の高い外部データ

新しいバージョン RHF は、values を使用したフォームへの外部データの同期をサポートします。

たとえば:

const form = useForm<FormData>({
  defaultValues: {
    username: "",
    email: "",
  },
  values: user
    ? {
        username: user.username,
        email: user.email,
      }
    : undefined,
});

defaultValues とは異なり、values は外部データの変更に応答します。

注: 外部データの更新は、ユーザーが現在編集している値にも影響を与える可能性があります。

したがって、サーバー上の既存のデータを編集するときは、ニーズに応じて選択できます。

  • 初期化は 1 回だけ: reset() または適切な values スキームを使用します。
  • サーバー データの継続的な同期: values を使用します。
  • ユーザーが変更したフィールドを保持: keepDirtyValues と組み合わせます。

14. Controller: 制御コンポーネントアダプター

RHF は、ネイティブ入力として register() を直接使用できます。

ただし、一部のサードパーティ UI コンポーネントは制御モードを使用します。次に例を示します。

  • Radix UI Select。
  • React Select。
  • 日付ピッカー。
  • カスタムSwitch。
  • カスタム組み合わせ入力コンポーネント。

このとき通常は Controller が使用されます。

import { Controller } from "react-hook-form";

<Controller
  name="city"
  control={control}
  render={({ field, fieldState }) => (
    <>
      <Select
        value={field.value}
        onValueChange={field.onChange}
        onBlur={field.onBlur}
      />

      {fieldState.error && <p>{fieldState.error.message}</p>}
    </>
  )}
/>;

Controllerの役割

Controller は、制御コンポーネントの値の変更を RHF に関連付けます。

通常提供されるもの:

field.name;
field.value;
field.onChange;
field.onBlur;
field.ref;

その中には:

  • value: 現在の値。
  • onChange: 新しい値を RHF に書き込みます。
  • onBlur: RHF フィールドにフォーカスを失ったことを通知します。
  • ref: 基礎となる入力要素を関連付け、フォーカスなどの機能をサポートします。

非ネイティブ コンポーネントの場合、ref を渡すことができるかどうかは、コンポーネントが対応する入力要素参照を公開するかどうかに直接依存します。

15. useController

useController() は、Controller と同様の機能を提供しますが、形式は Hook です。

const { field, fieldState, formState } = useController({
  name: "username",
  control,
});

使用:

<Input
  value={field.value}
  onChange={field.onChange}
  onBlur={field.onBlur}
  name={field.name}
  ref={field.ref}
/>

一般的なフォーム コンポーネントのカプセル化に適しています。

FormInput
FormSelect
FormSwitch
FormDatePicker

Controller および useController

コントラストControlleruseController
フォームReact ComponentReact Hook
主な目的サードパーティ製コンポーネントに直接適応再利用可能なフォームコンポーネントをカプセル化する
fieldを提供してくださいはいはい
fieldStateを提供してくださいはいはい

この 2 つは、下部に同様の制御メカニズムを使用します。通常、同じフィールドを同時に繰り返し登録する必要はなく、そのうちの 1 つを選択できます。

16. useFieldArray: 動的フィールド配列

useFieldArray() は、動的に追加、削除、または並べ替えできるフィールドの配列を管理するために使用されます。

たとえば、旅行の計画:

{
  destinations: [{ city: "Tokyo" }, { city: "Kyoto" }];
}

例

type FormData = {
  destinations: {
    city: string;
  }[];
};

const { register, control } = useForm<FormData>({
  defaultValues: {
    destinations: [{ city: "" }],
  },
});

const { fields, append, remove, update, move } = useFieldArray({
  control,
  name: "destinations",
});

レンダリング:

{
  fields.map((field, index) => (
    <input key={field.id} {...register(`destinations.${index}.city`)} />
  ));
}

新しい:

append({ city: "" });

削除:

remove(1);

移動:

move(0, 2);

一般的な方法

方法機能
append最後に追加
prepend先頭に追加
insert指定した位置に挿入
remove削除
swap2 つの要素を交換する
move要素の移動
update指定した項目を置換する
replace配列全体を置き換える

注意事項

使用する必要があります:

key={field.id}

単純に使用する代わりに:

key = { index };

配列の挿入または削除後にインデックスが変更される可能性があるためです。

さらに、update() により、対応するフィールドが再マウントされる可能性があります。特定のフィールドの値のみを変更する必要がある場合は、setValue() の使用を検討してください。

17. FormProviderとuseFormContext

複雑なフォームは多くの場合、サブコンポーネントに分割されます。

ProfileForm
├── BasicInfoSection
│   ├── UsernameField
│   └── EmailField
├── AddressSection
│   ├── CountryField
│   └── CityField
└── SubmitButton

レイヤーごとに渡される場合:

register;
control;
errors;
setValue;

Props が大量に生成されます。

FormProvider は、React Context を通じて RHF のフォーム コントローラーを共有できます。

親コンポーネント

import { FormProvider, useForm } from "react-hook-form";

function ProfileForm() {
  const methods = useForm<FormData>();

  return (
    <FormProvider {...methods}>
      <form onSubmit={methods.handleSubmit(onSubmit)}>
        <BasicInfoSection />
        <AddressSection />
        <SubmitButton />
      </form>
    </FormProvider>
  );
}

サブコンポーネント

function UsernameField() {
  const {
    register,
    formState: { errors },
  } = useFormContext<FormData>();

  return (
    <>
      <input {...register("username")} />

      {errors.username && <p>{errors.username.message}</p>}
    </>
  );
}

中心的な役割

useForm()
    ↓
FormProvider
    ↓
React Context
    ↓
useFormContext()
    ↓
Child Components

register、control、その他のメソッドをレイヤーごとに渡す必要はありません。

注: 通常、複数の FormProvider を同じ形式で不必要にネストしないでください。

18. useFormState: 部分的なフォーム ステータスのサブスクリプション

useFormState() により、コンポーネントは必要なフォーム状態のみをサブスクライブできます。

たとえば:

import { useFormState } from "react-hook-form";

function SubmitButton() {
  const { control } = useFormContext<FormData>();

  const { isDirty, isSubmitting } = useFormState({
    control,
  });

  return (
    <button type="submit" disabled={!isDirty || isSubmitting}>
      Save
    </button>
  );
}

このようにして、SubmitButton は関連するステータスを独立してサブスクライブできます。

useFormStateの値

大きなフォームの場合、フィールドが更新されるたびにフォームのルート コンポーネント全体を再レンダリングすることは避けてください。

特に次のような用途に適しています。

  • 独立したエラー メッセージ コンポーネント。
  • SubmitButton。
  • フィールドのグループ化。
  • 複雑なネストされたフォーム。

RHF のフォーム ステータスは、サブスクリプション ベースの最適化メカニズムを使用します。コンポーネントが本当に必要とするステータスのみを読み取ることをお勧めします。

19. trigger: 検証を積極的に実行する

trigger() フィールド検証は手動で実行できます。

フォーム全体を検証する

const valid = await trigger();

if (valid) {
  console.log("All fields are valid");
}

単一フィールドを検証する

await trigger("email");

複数のフィールドを検証する

await trigger(["username", "email"]);

一般的な用途

  • 複数ステップのフォーム。
  • 次のステップに進む前に確認してください。
  • フィールド A を変更した後、フィールド B を再検証します。
  • 特定のボタン操作の前に入力を積極的にチェックします。

たとえば:

const handleNextStep = async () => {
  const valid = await trigger(["username", "email"]);

  if (valid) {
    setStep(2);
  }
};

trigger() は、検証に合格したかどうかを示すブール値を返します。

20. setError: 手動設定エラー

setError() は、RHF にエラーを書き込むために使用されます。

setError("username", {
  type: "manual",
  message: "Username already exists",
});

その後、次のように渡すことができます。

errors.username?.message;

エラーメッセージを読んでください。

サーバー側の検証エラーを処理する

たとえば、Django DRF は次を返します。

{
  "username": ["This username already exists."]
}

サーバー側のエラーはフォーム フィールドにマッピングできます。

setError("username", {
  type: "server",
  message: "This username already exists.",
});

非フィールドレベルのエラー

たとえば、サーバーは次の一般的なエラーを返します。

setError("root.server", {
  type: "server",
  message: "Failed to save profile",
});

UI に表示:

{
  errors.root?.server && <p>{errors.root.server.message}</p>;
}

setError および isValid

setError() はエラー ステータスを手動で追加できますが、isValid の最終結果は主にフォーム検証ルールによって計算されます。

したがって、setError() は isValid を永続的に制御するスイッチとして理解すべきではありません。

登録されたフィールドに手動で設定されたエラーも、フィールドが再検証されるときにクリアされる場合があります。

21. clearErrors: エラーをクリア

clearErrors() は、フォーム エラーを手動でクリアするために使用されます。

個々のフィールドをクリアする

clearErrors("username");

複数のフィールドをクリアする

clearErrors(["username", "email"]);

すべてのエラーをクリアする

clearErrors();

サーバーエラーをクリアする

clearErrors("root.server");

たとえば、ユーザーがフィールドを再編集すると、以前のサーバー エラーをクリアできます。

<input
  {...register("username", {
    onChange: () => {
      clearErrors("root.server");
    },
  })}
/>

clearErrorsとtriggerの違い

方法機能
clearErrors()現在保存されているエラーをクリアする
trigger()検証を再実行する
setError()エラーを手動で追加する

重要: clearErrors() は検証ルールを変更しません。また、自動的に検証をトリガーしたり、isValid が true になることもありません。 値がまだ不正な場合は、次回検証したときにエラーが再発する可能性があります。

22. React Hook Form + TanStack Query

すでにサーバー上にデータがあるフォームを編集する場合の一般的なプロセスは次のとおりです。

TanStack Query
      ↓
GET User Profile
      ↓
React Hook Form
      ↓
User Edits Fields
      ↓
handleSubmit
      ↓
useMutation
      ↓
PATCH User Profile
      ↓
Update Query Cache
      ↓
reset(savedData)

例

type ProfileFormData = {
  username: string;
  email: string;
};

function ProfileForm({ initialData }: { initialData: ProfileFormData }) {
  const queryClient = useQueryClient();

  const {
    register,
    handleSubmit,
    reset,
    setError,
    formState: { errors, isDirty },
  } = useForm<ProfileFormData>({
    defaultValues: initialData,
  });

  const mutation = useUserProfilePartialUpdate();

  const onSubmit = async (data: ProfileFormData) => {
    try {
      const updatedProfile = await mutation.mutateAsync({
        data,
      });

      queryClient.setQueryData(
        getUserProfileRetrieveQueryKey(),
        updatedProfile,
      );

      reset({
        username: updatedProfile.username,
        email: updatedProfile.email,
      });
    } catch (error) {
      setError("root.server", {
        type: "server",
        message: "Unable to save profile",
      });
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("username")} />
      <input {...register("email")} />

      {errors.root?.server && <p>{errors.root.server.message}</p>}

      <button type="submit" disabled={!isDirty || mutation.isPending}>
        {mutation.isPending ? "Saving..." : "Save"}
      </button>
    </form>
  );
}

上記の例では、Mutation が完全な最新の Profile データを返すことを前提としています。

バックエンドが 204 または単純な成功ステータスのみを返す場合は、Mutation の戻り値を Profile キャッシュに直接書き込むのではなく、invalidateQueries() を使用してデータを再フェッチする必要があります。

保存後になぜ reset になるのでしょうか?

正常に保存された後、現在のデータはサーバーによって認識された新しい値になっているためです。

たとえば:

オリジナル username = Alice
        ↓
ユーザーが次のように変更されました Bob
        ↓
isDirty = true
        ↓
サーバー保存 Bob
        ↓
reset({ username: "Bob" })
        ↓
isDirty = false

このように、ユーザーが編集を続けると、直前に保存されたデータを基準にして Dirty ステータスが判断されます。

23. ファイルのアップロード

ファイルフィールドには以下を使用できます。

<input type="file" {...register("avatar")} />

通常、RHF は FileList を受け取ります。

たとえば:

type AvatarFormData = {
  avatar: FileList;
};

const onSubmit = async (data: AvatarFormData) => {
  const file = data.avatar?.[0];

  if (!file) return;

  const formData = new FormData();
  formData.append("avatar", file);

  await uploadAvatar(formData);
};

Orval によって生成されたアップロード関数の場合、FormData の繰り返し構築を避けるために、OpenAPI Schema と実際に生成されたパラメーター タイプが優先される必要があります。

さらに、ブラウザのファイル入力では、通常のテキスト入力のようにファイル値を任意に設定できません。したがって、ファイルフィールドのリセットとデフォルト値の処理は個別に考慮する必要があります。

24. パフォーマンスの最適化

React Hook Form の利点の 1 つは、不必要な再レンダリングが削減されることです。

一般的に使用される戦略:

1. 最初にネイティブ入力 register を使用します

<input {...register("username")} />

すべてのネイティブ Input に対して Controller を使用する必要はありません。

2. 複雑なコンポーネントは Controller を使用します

<Controller
  name="city"
  control={control}
  render={({ field }) => (
    <CustomSelect value={field.value} onValueChange={field.onChange} />
  )}
/>

3. 完全形式 watch を避ける

大きなフォームのトップレベルで無差別に使用することはお勧めできません。

watch();

代わりに、必要なサブコンポーネントで使用できます。

useWatch({
  control,
  name: "country",
});

4. 部分購読 formState

使用:

useFormState({
  control,
});

最上位コンポーネントによってすべての状態が読み取られてレンダリングされることは避けてください。

5. 完全な defaultValues を提供する

useForm<FormData>({
  defaultValues: {
    username: "",
    email: "",
  },
});

これは、Dirty ステータスを正確に判断するのに役立ち、制御コンポーネントが undefined の初期値に遭遇する問題を回避します。

6. reset を頻繁に行わないでください

サーバー Refetch が毎回 reset() を呼び出すと、ユーザーが編集中のデータが上書きされる可能性があります。

keepDirtyValues を使用することも、1 回だけ初期化されるストラテジーを作成することもできます。

25. よくある質問

Q1: React Hook Form は useState の代わりになりますか?

フォームフィールドの状態管理の一部であるuseStateのみ置き換え可能です。

Modal が開いているかどうか、現在の Tab、ページ UI ステータスなどは、依然として React State の使用に適しています。

Q2: RHFとTanStack Queryは重複する機能はありますか?

多少の重複はありますが、主な責任は異なります。

  • RHF: ユーザーによって送信されていない一時的なフォームのステータス。
  • TanStack Query: サーバー データ、キャッシュ、およびリクエストのステータス。

この 2 つは組み合わせて使用​​されることがよくあります。

Q3: すでに defaultValues を持っているのに、なぜ reset を使用する必要があるのですか?

defaultValues は主に初期化に使用されるためです。

通常、サーバー データは非同期で取得されるため、取得の完了後にフォームと同期する必要があります。 reset() は一般的に使用される方法の 1 つです。

Q4: Controller は register よりも高度ですか?

いいえ。

どちらも異なるコンポーネント タイプを対象としています。

通常、ネイティブ Input は register を使用する方が簡単です。複雑な制御コンポーネントには Controller の使用が適しています。

Q5: フォームの検証は合格しましたが、サーバーが依然として 400 を返すのはなぜですか?

フロントエンド検証はユーザー エクスペリエンスを最適化するだけであるため、サーバーがデータを受け入れるという保証はありません。

サーバーには以下が追加される場合があります。

  • データベースの一意の制約。
  • 許可チェック。
  • ビジネスルール。
  • 同時に状態が変化します。
  • フィールドの変換と検証。

したがって、Django Serializer のサーバー側検証を保持する必要があります。

概要

React Hook Form の中心的な内容は 4 つのレベルに分けることができます。

分類フォーカス
基本形useForm、register、handleSubmit、validation
フォームステータスformState、watch、getValues、setValue、reset
複雑なコンポーネントController、useController、useFieldArray、FormProvider
高度な機能resolver、trigger、setError、clearErrors、useWatch、useFormState

コアプロセス:

useForm()
    ↓
register / Controller
    ↓
Form Values
    ↓
Validation
    ↓
handleSubmit()
    ↓
TanStack Query Mutation
    ↓
Axios
    ↓
Django API
    ↓
Cache Update / Invalidation
    ↓
reset()

React + TypeScript + Orval + TanStack Query プロジェクトの場合、最も重要なことは以下を理解することです。

  1. registerとControllerの違い。
  2. defaultValues、isDirty、dirtyFieldsの関係。
  3. watch、useWatch、getValuesの違い。
  4. reset フォームベースを初期化および再定義する方法。
  5. setError を介してサーバー側の検証エラーがどのように表示されるか。
  6. mutateAsync を使用して非同期コミットを管理する方法。
  7. Mutation 成功後のサーバー キャッシュとフォームのステータスを同期する方法。

参考資料