mcpField
R
react-hook-form-zod

react-hook-form-zod

github.com/marcioaltoe開発050 回インストール1 日前に更新
リポジトリ

react-hook-form-zod について

React Hook Form と Zod を使ったフォーム構築の開発スキルです。検証、フィールド処理、エラー対応、パフォーマンスのパターンを扱い、複数ステップや動的フォームにも対応します。

主な機能

  1. 01zodResolver による Zod スキーマ検証
  2. 02型安全なフォーム設定とフィールド登録
  3. 03フィールド間および条件付き検証
  4. 04サーバーエラーのフィールドへの対応付け
  5. 05大規模・複数ステップフォームのパターン

活用シーン

  1. 01スキーマ検証付きの型安全な React フォーム構築
  2. 02動的リストや複数ステップフォームの処理
  3. 03サーバー検証エラーとフィールドの連携

インストール

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

$npx skills add https://github.com/marcioaltoe/skills --skill react-hook-form-zod

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

同梱ファイル

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

.claude-plugin/
plugin.json
agents/
openai.yaml
references/
accessibility.md
error-handling.md
links-to-official-docs.md
performance-optimization.md
rhf-api-reference.md
shadcn-integration.md
top-errors.md
zod-schemas-guide.md
rules/
react-hook-form-zod.md
scripts/
check-versions.sh
templates/
advanced-form.tsx
async-validation.tsx
basic-form.tsx
custom-error-display.tsx
dynamic-fields.tsx
multi-step-form.tsx
package.json
server-validation.ts
shadcn-form.tsx

詳細

React Hook Form + Zod によるバリデーション

ステータス:本番環境対応済み ✅ 最終確認日:2026-01-20 最新バージョン:[email protected], [email protected], @hookform/[email protected]


クイックスタート

npm install [email protected] [email protected] @hookform/[email protected]

基本的なフォームパターン:

const schema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
})

// zodResolver infers types — no need for z.infer<typeof schema> on useForm
const form = useForm({
  resolver: zodResolver(schema),
  defaultValues: { email: '', password: '' }, // REQUIRED to prevent uncontrolled warnings
})

const onSubmit = form.handleSubmit((value) => {
  console.log(value)
})

<form onSubmit={onSubmit}>
  <input {...form.register('email')} />
  {form.formState.errors.email && <span role="alert">{form.formState.errors.email.message}</span>}
</form>

サーバー側のバリデーション(重要:絶対に省略しないでください):

// SAME schema on server
const data = schema.parse(await req.json())

主なパターン

useForm のオプション(バリデーションモード):

  • mode: 'onSubmit'(デフォルト)- パフォーマンスに最適
  • mode: 'onBlur' - バランスが良好
  • mode: 'onChange' - リアルタイムにフィードバックを得られますが、再レンダリングが増えます
  • shouldUnregister: true - アンマウント時にフィールドのデータを削除します(複数ステップのフォームで使用します)

Zod のリファインメント(フィールド間のバリデーション):

z.object({ password: z.string(), confirm: z.string() })
  .refine((data) => data.password === data.confirm, {
    message: "Passwords don't match",
    path: ['confirm'], // CRITICAL: Error appears on this field
  })

Zod の変換:

z.string().transform((val) => val.toLowerCase()) // Data manipulation
z.string().transform(parseInt).refine((v) => v > 0) // Chain with refine

Zod v4.3.0 以降の機能:

// Exact optional (can omit field, but NOT undefined)
z.string().exactOptional()

// Exclusive union (exactly one must match)
z.xor([z.string(), z.number()])

// Import from JSON Schema
z.fromJSONSchema({ type: "object", properties: { name: { type: "string" } } })

zodResolver は Zod と React Hook Form を接続し、型安全性を維持します


登録

register(標準的な HTML 入力要素向け):

<input {...form.register('email')} /> // Uncontrolled, best performance

Controller(サードパーティ製コンポーネント向け):

<Controller
  name="category"
  control={form.control}
  render={({ field }) => <CustomSelect {...field} />} // MUST spread {...field}
/>

Controller を使う場合:React Select、日付ピッカー、ref に対応していないカスタムコンポーネントでは Controller を使います。それ以外の場合は register を使います。


エラー処理

エラーの表示:

{form.formState.errors.email && <span role="alert">{form.formState.errors.email.message}</span>}
{form.formState.errors.address?.street?.message} // Nested errors (use optional chaining)

サーバーエラー:

const onSubmit = form.handleSubmit(async (data) => {
  const res = await fetch('/api/submit', { method: 'POST', body: JSON.stringify(data) })
  if (!res.ok) {
    const { errors: serverErrors } = await res.json()
    Object.entries(serverErrors).forEach(([field, msg]) => form.setError(field, { message: msg }))
  }
})

高度なパターン

useFieldArray(動的なリスト):

const { fields, append, remove } = useFieldArray({ control: form.control, name: 'contacts' })

{fields.map((field, index) => (
  <div key={field.id}> {/* CRITICAL: Use field.id, NOT index */}
    <input {...form.register(`contacts.${index}.name` as const)} />
    {form.formState.errors.contacts?.[index]?.name && <span>{form.formState.errors.contacts[index].name.message}</span>}
    <button onClick={() => remove(index)}>Remove</button>
  </div>
))}
<button onClick={() => append({ name: '', email: '' })}>Add</button>

非同期バリデーション(デバウンス):

const debouncedValidation = useDebouncedCallback(() => form.trigger('username'), 500)

複数ステップのフォーム:

const step1 = z.object({ name: z.string(), email: z.string().email() })
const step2 = z.object({ address: z.string() })
const fullSchema = step1.merge(step2)

const nextStep = async () => {
  const isValid = await form.trigger(['name', 'email']) // Validate specific fields
  if (isValid) setStep(2)
}

条件付きバリデーション:

z.discriminatedUnion('accountType', [
  z.object({ accountType: z.literal('personal'), name: z.string() }),
  z.object({ accountType: z.literal('business'), companyName: z.string() }),
])

shouldUnregister を使った条件付きフィールド:

const form = useForm({
  resolver: zodResolver(schema),
  shouldUnregister: false, // Keep values when fields unmount (default)
})

// Or use conditional schema validation:
z.object({
  showAddress: z.boolean(),
  address: z.string(),
}).refine((data) => {
  if (data.showAddress) {
    return data.address.length > 0;
  }
  return true;
}, {
  message: "Address is required",
  path: ["address"],
})

shadcn/ui との統合

注:shadcn/ui では Form コンポーネントは非推奨になりました。新しい実装では Field コンポーネントを使ってください(最新のドキュメントを確認してください)。

よくあるインポートミス:IDE や AI が shadcn ではなく "react-hook-form" から Form を自動インポートすることがあります。必ず次のようにインポートしてください:

// ✅ Correct:
import { useForm } from "react-hook-form";
import { Form, FormField, FormItem } from "@/components/ui/form"; // shadcn

// ❌ Wrong (auto-import mistake):
import { useForm, Form } from "react-hook-form";

レガシーの Form コンポーネント:

<FormField control={form.control} name="username" render={({ field }) => (
  <FormItem>
    <FormControl><Input {...field} /></FormControl>
    <FormMessage />
  </FormItem>
)} />

パフォーマンス

  • 標準的な入力要素には、Controller(制御コンポーネント)より register(非制御コンポーネント)を使います
  • watch() ではなく watch('email') を使います(特定のフィールドに再レンダリングを限定できます)
  • 複数ステップのフォームでは shouldUnregister: true を使います(アンマウント時にデータを消去します)
大規模なフォーム(300 以上のフィールド)

警告:resolver(Zod/Yup)を使用し、さらに formState のプロパティを参照するフォームでは、登録時に処理が 10~15 秒間停止することがあります。(Issue #13129)

パフォーマンス特性:

  • クリーン(resolver なし、formState の参照なし):ほぼ即時
  • resolver のみ使用:ほぼ即時
  • formState の参照のみ:ほぼ即時
  • resolver と formState の参照の両方を使用:300 フィールドで約 9.5 秒

回避策:

  1. formState の分割代入を避ける - 必要なときにのみ、プロパティをインラインで参照します:
// ❌ Slow with 300+ fields:
const { isDirty, isValid } = form.formState;

// ✅ Fast:
const handleSubmit = () => {
  if (!form.formState.isValid) return; // Read inline only when needed
};
  1. mode: "onSubmit" を使う - 変更のたびにバリデーションしないようにします:
const form = useForm({
  resolver: zodResolver(largeSchema),
  mode: "onSubmit", // Validate only on submit, not onChange
});
  1. サブフォームに分割する - 個別のスキーマを使って、複数の小さなフォームに分けます:
// Instead of one 300-field form, use 5-6 forms with 50-60 fields each
const form1 = useForm({ resolver: zodResolver(schema1) }); // Fields 1-50
const form2 = useForm({ resolver: zodResolver(schema2) }); // Fields 51-100
  1. フィールドを遅延レンダリングする - タブやアコーディオンを使い、表示中のフィールドだけをマウントします:
// Only mount fields for active tab, reduces initial registration time
{activeTab === 'personal' && <PersonalInfoFields />}
{activeTab === 'address' && <AddressFields />}

重要なルール

✅ 常に defaultValues を設定する(非制御から制御への切り替えに関する警告を防ぎます)

✅ クライアント側とサーバー側の両方でバリデーションする(クライアント側の処理は回避可能です。セキュリティ上の問題につながります!)

✅ useFieldArray ではキーに field.id を使う(index は使わないでください)

✅ const form = useForm(...) を使う — form.control、form.handleSubmit、form.formState、form.reset() などを通じてアクセスします。分割代入しないでください。

✅ const onSubmit = form.handleSubmit((value) => {...}) — 送信ハンドラーを取り出し、onSubmit={} に直接記述しないでください。

⚪ z.infer<typeof schema> は省略可能です — zodResolver が自動的に型を推論します。型を別の場所(関数シグネチャや props など)で明示的に使う必要がある場合にのみ使用してください。

❌ サーバー側のバリデーションを絶対に省略しないでください(セキュリティ上の脆弱性につながります)

❌ 値を直接変更しないでください(form.setValue() を使います)

❌ 制御パターンと非制御パターンを混在させないでください

❌ useFieldArray ではキーに index を使わないでください


既知の問題(20 件の予防策)

  1. Zod v4 の型推論 - #13109:z.infer<typeof schema> を明示的に使用してください。v7.66.x 以降で解決済みです。注:@hookform/resolvers は Zod v4 との TypeScript 互換性に問題があります(#813)。回避策:import { z } from 'zod/v3' を使用するか、resolver の更新をお待ちください。

  2. 非制御から制御への警告 - すべてのフィールドに defaultValues を必ず設定してください

  3. ネストされたオブジェクトのエラー - オプショナルチェーンを使用してください:errors.address?.street?.message

  4. 配列フィールドの再レンダリング - useFieldArray では(インデックスではなく)key={field.id} を使用してください

  5. 非同期バリデーションの競合状態 - バリデーションをデバウンスし、保留中のリクエストをキャンセルしてください

  6. サーバーエラーのマッピング - setError() を使用して、サーバーエラーをフィールドにマッピングしてください

  7. デフォルト値が適用されない - useForm のオプションに defaultValues を設定してください(useState ではありません)

  8. Controller フィールドが更新されない - render 関数で常に {...field} を展開してください

  9. useFieldArray のキーに関する警告 - キーには(インデックスではなく)field.id を使用してください

  10. スキーマのリファインメントでのエラーパス - リファインメントで path を指定してください:refine(..., { path: ['fieldName'] })

  11. transform と preprocess の使い分け - 出力には transform、入力には preprocess を使用してください

  12. 複数の resolver の競合 - 単一の resolver(zodResolver)を使用し、必要に応じてスキーマを結合してください

  13. Zod v4 のオプショナルフィールドのバグ - #13102:オプショナルフィールド(.optional())に空文字列 "" を設定すると、誤ってバリデーションエラーが発生します。回避策:.nullish()、.or(z.literal(""))、または z.preprocess((val) => val === "" ? undefined : val, z.email().optional()) を使用してください

  14. useFieldArray はプリミティブ型の配列をサポートしない - #12570:設計上の制限です。useFieldArray が扱えるのはオブジェクトの配列のみで、string[] のようなプリミティブ型の配列には対応していません。回避策:プリミティブ値をオブジェクトでラップしてください。["string"] ではなく [{ value: "string" }] を使用します

  15. useFieldArray の SSR ID 不一致 - #12782:SSR(Remix、Next.js)でハイドレーションの不一致に関する警告が発生します。サーバーで生成されたフィールド ID がクライアントと一致しません。回避策:フィールド配列をクライアントのみでレンダリングするか、V8 をお待ちください(決定的な key を使用します)

  16. Next.js 16 の reset() バリデーションのバグ - #13110:Server Actions の送信後に form.reset() を呼び出すと、次の送信時にバリデーションエラーが発生します。v7.65.0 以降で修正済みです。修正前の回避策:reset() ではなく setValue() を使用してください

  17. バリデーションの競合状態 - #13156:resolver のバリデーション中に、isValidating=false でありながら errors がまだ設定されていない中間状態でレンダリングされます。エラーだけを基に有効性を判定しないでください。次のように使用してください:!errors.field && !isValidating

  18. ベータ版で ZodError がスローされる - #12816:Zod v4 のベータ版では、ZodError が formState.errors に格納されず、直接スローされます。安定版の Zod v4.1.x 以降で修正済みです。ベータ版は避けてください

  19. 大規模なフォームのパフォーマンス - #13129:resolver と formState の読み取りを伴う 300 個以上のフィールドでは、10~15 秒間フリーズします。4 つの回避策については、パフォーマンスのセクションを参照してください

  20. shadcn Form のインポートに関する混乱 - IDE や AI が、shadcn ではなく "react-hook-form" から Form を自動インポートする場合があります。Form コンポーネントは必ず @/components/ui/form からインポートしてください


V8 で予定されている変更(ベータ)

React Hook Form v8(v8.0.0-beta.1 としてベータ版が公開中。リリース日は 2026-01-11)では、破壊的変更が導入されます。RFC の議論 #7433

破壊的変更:

  1. useFieldArray:id → key:
// V7:
const { fields } = useFieldArray({ control, name: "items" });
fields.map(field => <div key={field.id}>...</div>)

// V8:
const { fields } = useFieldArray({ control, name: "items" });
fields.map(field => <div key={field.key}>...</div>)
// keyName prop removed
  1. Watch コンポーネント:names → name:
// V7:
<Watch names={["email", "password"]} />

// V8:
<Watch name={["email", "password"]} />
  1. watch() のコールバック API を削除:
// V7:
watch((data, { name, type }) => {
  console.log(data, name, type);
});

// V8: Use useWatch or manual subscription
const data = useWatch({ control });
useEffect(() => {
  console.log(data);
}, [data]);
  1. setValue() で useFieldArray が更新されなくなりました:
// V7:
setValue("items", newArray); // Updates field array

// V8: Must use replace() API
const { replace } = useFieldArray({ control, name: "items" });
replace(newArray);

V8 のメリット:

  • SSR のハイドレーション不一致を修正(ランダムな id ではなく、決定論的な key を使用)
  • パフォーマンスが向上
  • TypeScript の型推論が改善

移行のスケジュール:V8 はベータ版です。安定版のリリース日は未定です。安定版のリリース状況はリリースでご確認ください。


同梱リソース

テンプレート:basic-form.tsx、advanced-form.tsx、shadcn-form.tsx、server-validation.ts、async-validation.tsx、dynamic-fields.tsx、multi-step-form.tsx、package.json

リファレンス:zod-schemas-guide.md、rhf-api-reference.md、error-handling.md、performance-optimization.md、shadcn-integration.md、top-errors.md

ドキュメント:https://react-hook-form.com/ | https://zod.dev/ | https://ui.shadcn.com/docs/components/form


ライセンス:MIT | 最終確認日:2026-01-20 | スキルバージョン:2.1.0 | 変更内容:既知の問題を 8 件追加(Zod v4 の optional フィールドのバグ、useFieldArray でプリミティブ値を使用する際の制限、SSR のハイドレーション不一致、大規模なフォームのパフォーマンスに関するガイダンス、Next.js 16 の reset() のバグ、バリデーションの競合状態、ベータ版で ZodError がスローされる問題、shadcn のインポートに関する混乱)、Zod v4.3.0 の機能(.exactOptional()、.xor()、z.fromJSONSchema())を追加、shouldUnregister を使用した条件付きフィールドのパターンを追加、V8 のベータ版の破壊的変更のセクションを追加、Zod v4 リゾルバーの互換性に関する注記を拡充、[email protected] に更新

コメント