mcpField
W
web-forms-react-hook-form

web-forms-react-hook-form

github.com/agents-inc開発2660 回インストール2 日前に更新
リポジトリ

web-forms-react-hook-form について

ReactフォームでReact Hook Formの実装パターンを扱う実用スキルです。フォーム状態や検証に加え、制御コンポーネント、繰り返し行、不要な再レンダリングを抑える購読方法を説明します。

主な機能

  1. 01型付きフォームと初期値の設定
  2. 02Controllerによる制御コンポーネント連携
  3. 03resolverを使ったスキーマ検証
  4. 04useFieldArrayによる繰り返し行
  5. 05フォーム状態の購読範囲の限定

活用シーン

  1. 01検証付きのアクセシブルなフォーム構築
  2. 02カスタム選択欄や日付ピッカーの連携
  3. 03追加・削除・並べ替え可能な行の管理
  4. 04大規模フォームの再レンダリング削減

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/agents-inc/skills --skill web-forms-react-hook-form

インストール後、Claude Code で/web-forms-react-hook-formを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

metadata.yaml
reference.md
examples/
arrays.md
controlled-components.md
core.md
form-options.md
performance.md
validation.md
wizard.md

詳細

React Hook Form のパターン

クイックガイド: register はネイティブ入力要素をバインドし、非制御のまま保持します。独自の値を保持するコンポーネントには Controller を使用します。繰り返し行は useFieldArray で管理し、field.id をキーにします。バリデーションは register のルール、または resolver を通じたスキーマのいずれかで指定できます。注意すべきなのは再レンダリングです。useWatch と useFormState は名前を指定したフィールドを購読しますが、watch() と formState の広範な分割代入はフォーム全体を購読します。

詳細なリソース:

  • examples/core.md — register、エラー表示、アクセシビリティ属性を備えた型安全なフォーム
  • examples/controlled-components.md — セレクト、日付ピッカー、チェックボックスグループをラップする Controller
  • examples/validation.md — resolver を通じてスキーマを組み込む方法
  • examples/arrays.md — ライブ合計を表示する useFieldArray の明細行
  • examples/performance.md — 分離された購読、FormStateSubscribe、useWatch の exact と compute
  • examples/form-options.md — 非同期データに対応する values プロパティ、disabled、<Form /> コンポーネント
  • examples/wizard.md — ステップごとに trigger() を使う複数ステップのフォーム
  • reference.md — 選択フローチャート、API テーブル、バージョンごとの機能一覧

該当する方法

  • ネイティブの input、select、または textarea — register は ref をフォームに渡し、フィールドは非制御のまま保たれるため、入力しても再レンダリングは発生しません。examples/core.md を参照してください。
  • 独自の値を保持するコンポーネント — カスタムセレクト、日付ピッカー、リッチテキストエディターなど、使用可能な ref を公開しないコンポーネントには、Controller が value と onChange を渡し、再レンダリングをそのフィールドに限定します。examples/controlled-components.md を参照してください。
  • register のルールではなくスキーマで指定するバリデーション — resolver を通じて渡すと、フィールドレベルの rules は不要になります。examples/validation.md を参照してください。

<critical_requirements>

React Hook Form のコードを書く前に

useForm<FormData>() をジェネリック型とともに呼び出し、すべてのフィールドに defaultValues を設定します。 ジェネリック型によって各フィールドのパスと送信ペイロードの型が決まり、デフォルト値によって最初のレンダーからすべての入力が制御対象としてマウントされます。

useFieldArray の行には field.id をキーとして使います。 これは RHF が行に割り当てる ID で、追加、削除、並べ替えを行っても維持されます。配列のインデックスではそうなりません。

コンポーネントが独自の値を保持する場合は、すぐに Controller を使います。 register にはネイティブ入力まで届く ref が必要です。ref を転送しないコンポーネントはフォームに参加できません。

mode は "onBlur" または "onTouched" に設定します。 デフォルトの "onSubmit" では最初の送信までフィードバックが表示されず、"onChange" ではキー入力のたびに検証が実行されます。

スキーマ検証は resolver 経由で渡し、スキーマは独立したモジュールに配置します。 コンポーネントの外に置いたスキーマは単独でテストでき、複数のフォームで再利用できます。フォーム側に残るのは配線だけです。

</critical_requirements>


自動検出: react-hook-form、useForm、register、handleSubmit、formState、Controller、 useFieldArray、useWatch、useFormContext、useFormState、FormProvider、FormStateSubscribe、SubmitHandler、 resolver、shouldUnregister、valueAsNumber

対象範囲:

  • React におけるフォームの状態、送信、検証の配線
  • 追加、削除、並べ替えが可能な繰り返しフィールドグループ
  • 独自の値を保持し、フォームとの橋渡しが必要なコンポーネント
  • 1つのフォームが複数の画面にまたがるマルチステップフロー
  • フィールド数の多いフォームでの再レンダーの抑制

別途扱う内容:

  • 検証スキーマの作成 — resolver はスキーマオブジェクトを受け取り、このスキルではフォームへの組み込みのみを扱います。スキーマでルールをどう記述するかは、それを管理する側で決まります。
  • 初期値の取得元 — フォームは props または values オプションを通じて初期値を受け取り、自身で取得することはありません。
  • 入力のマークアップとスタイル — すべての例では標準的な要素を使っているため、クラス名は自由に設定できます。

フォームの状態は React の状態の外部にあります。入力はフォームに自ら登録し、ref を通じて値を報告するため、キー入力によってフォーム独自のストアが更新されても、フィールドを所有するコンポーネントは再レンダーされません。エラーメッセージ、計算された合計、送信ボタンなど、フォームの状態を読み取るものはすべて、名前付きのスライスを購読します。何も購読しないコンポーネントは再レンダーされません。

これが、広範な読み取りに大きなコストがかかる理由です。レンダー本体での watch() と、6つのプロパティを取り出す formState の分割代入は、どちらもフォーム全体を購読し、ライブラリが実現するよう設計された分離を損ないます。


基本パターン

パターン 1: ジェネリック型を指定した useForm

ジェネリック型、mode、defaultValues を組み合わせることで、型安全性、検証のタイミング、制御対象入力に関する警告が決まります。

const {
  register,
  handleSubmit,
  formState: { errors, isSubmitting },
} = useForm<ContactFormData>({
  mode: "onBlur",
  defaultValues: { name: "", email: "", message: "" },
});

完全なコード:examples/core.md


パターン 2: 独自の値を保持するコンポーネントに Controller を使う

Controller はフィールド自体をレンダーし、value と onChange を渡します。どちらを使うかの判断基準は、ref がネイティブ入力に転送されるコンポーネントなら register が使え、それ以外では Controller が必要ということです。

<Controller
  name="service"
  control={control}
  rules={{ required: "Service is required" }}
  render={({ field, fieldState: { error } }) => (
    <>
      <Select {...field} options={serviceOptions} />
      {error && <span role="alert">{error.message}</span>}
    </>
  )}
/>

完全なコード:examples/controlled-components.md


パターン 3: 繰り返し行に useFieldArray を使う

fields には行ごとに生成された id が含まれており、これを React のキーとして使います。配列全体に対するルールはフックに指定し、エラーは errors.items.root に格納されます。

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

{fields.map((field, index) => (
  <div key={field.id}>
    <input {...register(`items.${index}.name`)} />
    <button type="button" onClick={() => remove(index)}>Remove</button>
  </div>
))}

完全なコード:examples/arrays.md


パターン 4: resolver によるスキーマ検証

resolver はフィールドごとの rules に代わるものです。スキーマが有効性を判断し、フィールドのパスに対してエラーを報告し、フォームは配線を担当します。

const { register, handleSubmit } = useForm<FormData>({
  resolver: zodResolver(schema),
  mode: "onBlur",
  defaultValues: { username: "", email: "" },
});

独立したモジュールに置いたスキーマは、レンダーせずにテストでき、複数のフォームで再利用できます。また、パスワードの一致や日付範囲など、フィールドごとの rules では表現できないフィールド間のルールも記述できます。

完全なコード:examples/validation.md


パターン 5:派生値に useWatch を使う

子コンポーネント内の useWatch は名前付きフィールドを購読するため、フィールドが変更されたときに再レンダリングされるのはその子コンポーネントだけです。

function PriceDisplay({ control }: { control: Control<PricingFormData> }) {
  const [plan, seats] = useWatch({ control, name: ["plan", "seats"] });
  return <div>Total: ${PLAN_PRICES[plan] * seats}</div>;
}

compute オプションを使うと、購読対象をさらに絞り込めます。入力値が変わったときではなく、計算結果が変わったときにコンポーネントが再レンダリングされます。

完全なコード:examples/performance.md のパターン 11 とパターン 12


パターン 6:ネストされたフィールドに useFormContext を使う

FormProvider はフォームメソッドをコンテキストに設定するため、ネストされたセクションや再利用されるセクションは、props のバケツリレーをせずにメソッドを利用できます。ネストが 3 階層ある場合や、セクションが複数回レンダリングされる場合に便利です。それより浅い場合は、control を渡すほうが簡単です。

<FormProvider {...methods}>
  <form onSubmit={methods.handleSubmit(onSubmit)}>
    <AddressFields prefix="shippingAddress" />
    <AddressFields prefix="billingAddress" />
  </form>
</FormProvider>

function AddressFields({ prefix }) {
  const { register } = useFormContext<CheckoutFormData>();
  return <input {...register(`${prefix}.street`)} />;
}

完全なコード:examples/wizard.md


パターン 7:外部データの読み込み

values はリアクティブで、データの変更に合わせてフォームも更新されます。一方、defaultValues はマウント時に一度だけ読み込まれます。values と resetOptions: { keepDirtyValues: true } を組み合わせると、バックグラウンドでの更新によってユーザーの入力内容が失われるのを防げます。

useForm<FormData>({
  values: userData,
  resetOptions: { keepDirtyValues: true },
});

保存が成功した後に reset(data) を呼び出すと、値とデフォルト値が一緒に置き換わり、isDirty がクリアされます。引数なしで reset() を呼び出すと、元のデフォルト値に戻ります。これがキャンセルボタンの動作です。

完全なコード:examples/form-options.md のパターン 7


パターン 8:エラー表示の分離

name を指定した useFormState は、そのフィールドの状態が変わったときにだけ再レンダリングされます。これにより、エラーメッセージが周囲のフォームを再レンダリングするのを防げます。

function FieldError<T extends FieldValues>({ control, name }: Props<T>) {
  const { errors } = useFormState({ control, name });
  const error = errors[name];
  if (!error) return null;
  return <span role="alert">{error.message as string}</span>;
}

完全なコード:examples/performance.md — フックについてはパターン 5、FormStateSubscribe コンポーネント形式についてはパターン 10


<red_flags>

注意事項

実行時に問題が発生します:

  • useFieldArray のキーに配列インデックスを使うと、React が誤った行を対応付けます。そのため、途中の項目を削除すると、それより下の値がすべて 1 つ上にずれます。field.id をキーにしてください。
  • 独自の値を保持するコンポーネントで register を使うと、ref が渡されず、フィールドが登録されません。その結果、送信データに値が含まれません。Controller でラップしてください。
  • defaultValues を指定しないと、入力欄は非制御でマウントされ、最初のキー入力で制御に切り替わります。React の警告が出て、SSR ではハイドレーションの不一致として報告されます。"" も含め、すべてのフィールドに初期値を設定してください。
  • append、prepend、insert に部分的なオブジェクトを渡すと、存在しないキーの値が undefined になり、入力欄が非制御として扱われます。完全な項目を渡してください。
  • onSubmit 内でエラーがスローされても、handleSubmit は捕捉しません。そのため、拒否が未処理のまま伝播します。コールバック内で捕捉してください。
  • フィールド配列自体の名前に対して setValue を使うと、行 ID が値に追従しません。replace() を使ってください。

予想外の動作:

  • useForm にジェネリック型を指定しないと、フィールドパスと送信データの型が any になり、register("emial") のような誤りも黙って受け入れられます。
  • 複数の formState プロパティを分割代入すると、それらすべてが購読対象になり、どれかが変わるたびにフォームが再レンダリングされます。コンポーネントが読み取るものだけを取得するか、useFormState で分離してください。
  • レンダー本体で watch() を使うと、すべてのフィールドを購読します。子コンポーネント内で useWatch を使うと、名前付きフィールドに絞り込めます。
  • shouldValidate: true を指定せずに setValue を使うと、以前のエラーが表示されたままになります。
  • 配列全体のエラーは errors.items.root に、各項目のエラーは errors.items[index].field に格納されます。
  • shouldUnregister: true にすると、アンマウントされたフィールドの値が破棄されます。フィールドを非表示にする機能、特にウィザードでは、false(デフォルト)のままにしてください。
  • useWatch は、購読が設定される前の最初のレンダーでは defaultValue を返します。
  • values は変化し続ける外部データ用、defaultValues は静的な初期値用です。両方を指定した場合、どちらが優先されるかは resetOptions によって変わります。
  • ステップごとの検証には trigger(fieldNames) が必要です。isValid はフォーム全体を反映するため、それを条件にしたウィザードは最初のステップから進めなくなります。

これらの中でよくある問題について、修正前と修正後のコード例をreference.mdに掲載しています。

</red_flags>

コメント