react-hook-form-zod について
React Hook FormとZodを組み合わせて使う、Reactフォーム開発者向けの実践ガイドです。バリデーション、エラー処理、動的フィールド、大規模フォームの性能上の注意点を扱います。
主な機能
- 01React Hook FormとZodの連携
- 02クライアントとサーバーの検証
- 03フィールドエラーの処理と割り当て
- 04動的フィールドと複数ステップフォーム
- 05大規模フォームの性能ガイド
活用シーン
- 01型推論を使ったReactフォームの構築
- 02フィールド間や条件付き入力の検証
- 03サーバーエラーのフォーム項目への表示
- 04大規模フォームの性能問題の調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ jezweb/ claude-skills --skill react-hook-form-zodインストール後、Claude Code で/react-hook-form-zodを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
README.md.claude-plugin/plugin.jsonreferences/accessibility.mderror-handling.mdlinks-to-official-docs.mdperformance-optimization.mdrhf-api-reference.mdshadcn-integration.mdtop-errors.mdzod-schemas-guide.mdrules/react-hook-form-zod.mdscripts/check-versions.shtemplates/advanced-form.tsxasync-validation.tsxbasic-form.tsxcustom-error-display.tsxdynamic-fields.tsxmulti-step-form.tsxpackage.jsonserver-validation.tsshadcn-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),
})
type FormData = z.infer<typeof schema>
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: { email: '', password: '' }, // REQUIRED to prevent uncontrolled warnings
})
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email')} />
{errors.email && <span role="alert">{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 {...register('email')} /> // Uncontrolled, best performance
Controller(サードパーティ製コンポーネント向け):
<Controller
name="category"
control={control}
render={({ field }) => <CustomSelect {...field} />} // MUST spread {...field}
/>
Controller を使用する場合:React Select、日付ピッカー、ref に対応していないカスタムコンポーネントです。それ以外の場合は register を使用してください。
エラー処理
エラーの表示:
{errors.email && <span role="alert">{errors.email.message}</span>}
{errors.address?.street?.message} // Nested errors (use optional chaining)
サーバーエラー:
const onSubmit = 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]) => setError(field, { message: msg }))
}
}
高度なパターン
useFieldArray(動的なリスト):
const { fields, append, remove } = useFieldArray({ control, name: 'contacts' })
{fields.map((field, index) => (
<div key={field.id}> {/* CRITICAL: Use field.id, NOT index */}
<input {...register(`contacts.${index}.name` as const)} />
{errors.contacts?.[index]?.name && <span>{errors.contacts[index].name.message}</span>}
<button onClick={() => remove(index)}>Remove</button>
</div>
))}
<button onClick={() => append({ name: '', email: '' })}>Add</button>
非同期バリデーション(デバウンス):
const debouncedValidation = useDebouncedCallback(() => 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 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 秒
回避策:
- formState の分割代入を避ける - 必要な場合にのみ、プロパティをインラインで読み取ります:
// ❌ Slow with 300+ fields:
const { isDirty, isValid } = form.formState;
// ✅ Fast:
const handleSubmit = () => {
if (!form.formState.isValid) return; // Read inline only when needed
};
- mode: "onSubmit" を使用する - 変更のたびにバリデーションを行わないようにします:
const form = useForm({
resolver: zodResolver(largeSchema),
mode: "onSubmit", // Validate only on submit, not onChange
});
- サブフォームに分割する - 個別のスキーマを持つ小規模なフォームを複数作成します:
// 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
- フィールドを遅延レンダリングする - タブやアコーディオンを使用し、表示中のフィールドのみをマウントします:
// Only mount fields for active tab, reduces initial registration time
{activeTab === 'personal' && <PersonalInfoFields />}
{activeTab === 'address' && <AddressFields />}
重要なルール
✅ 必ず defaultValues を設定する(非制御から制御への切り替えに関する警告を防ぎます)
✅ クライアントとサーバーの両方でバリデーションする(クライアント側のチェックは回避できるため、セキュリティ上重要です!)
✅ useFieldArray では キーに field.id を使用する(index は使用しないでください)
✅ Controller の render では {...field} を展開する
✅ 型推論には z.infer<typeof schema> を使用する
❌ サーバー側のバリデーションを絶対に省略しない(セキュリティ上の脆弱性につながります)
❌ 値を直接変更しない(setValue() を使用してください)
❌ 制御パターンと非制御パターンを混在させない
❌ useFieldArray では キーに index を使用しない
既知の問題(20 件の予防策)
-
Zod v4 の型推論 - #13109:
z.infer<typeof schema>を明示的に使用してください。v7.66.x 以降で解決済みです。注:@hookform/resolvers は Zod v4 との TypeScript 互換性に問題があります(#813)。回避策:import { z } from 'zod/v3'を使用するか、resolver の更新をお待ちください。 -
非制御から制御への警告 - すべてのフィールドに
defaultValuesを必ず設定してください -
ネストされたオブジェクトのエラー - オプショナルチェーンを使用してください:
errors.address?.street?.message -
配列フィールドの再レンダリング - useFieldArray では(インデックスではなく)
key={field.id}を使用してください -
非同期バリデーションの競合状態 - バリデーションをデバウンスし、保留中のリクエストをキャンセルしてください
-
サーバーエラーのマッピング -
setError()を使用して、サーバーエラーをフィールドにマッピングしてください -
デフォルト値が適用されない - useForm のオプションに
defaultValuesを設定してください(useState ではありません) -
Controller フィールドが更新されない - render 関数で常に
{...field}を展開してください -
useFieldArray のキーに関する警告 - キーには(インデックスではなく)
field.idを使用してください -
スキーマのリファインメントでのエラーパス - リファインメントで
pathを指定してください:refine(..., { path: ['fieldName'] }) -
transform と preprocess の使い分け - 出力には
transform、入力にはpreprocessを使用してください -
複数の resolver の競合 - 単一の resolver(zodResolver)を使用し、必要に応じてスキーマを結合してください
-
Zod v4 のオプショナルフィールドのバグ - #13102:オプショナルフィールド(
.optional())に空文字列""を設定すると、誤ってバリデーションエラーが発生します。回避策:.nullish()、.or(z.literal(""))、またはz.preprocess((val) => val === "" ? undefined : val, z.email().optional())を使用してください -
useFieldArray はプリミティブ型の配列をサポートしない - #12570:設計上の制限です。
useFieldArrayが扱えるのはオブジェクトの配列のみで、string[]のようなプリミティブ型の配列には対応していません。回避策:プリミティブ値をオブジェクトでラップしてください。["string"]ではなく[{ value: "string" }]を使用します -
useFieldArray の SSR ID 不一致 - #12782:SSR(Remix、Next.js)でハイドレーションの不一致に関する警告が発生します。サーバーで生成されたフィールド ID がクライアントと一致しません。回避策:フィールド配列をクライアントのみでレンダリングするか、V8 をお待ちください(決定的な
keyを使用します) -
Next.js 16 の reset() バリデーションのバグ - #13110:Server Actions の送信後に
form.reset()を呼び出すと、次の送信時にバリデーションエラーが発生します。v7.65.0 以降で修正済みです。修正前の回避策:reset()ではなくsetValue()を使用してください -
バリデーションの競合状態 - #13156:resolver のバリデーション中に、
isValidating=falseでありながらerrorsがまだ設定されていない中間状態でレンダリングされます。エラーだけを基に有効性を判定しないでください。次のように使用してください:!errors.field && !isValidating -
ベータ版で ZodError がスローされる - #12816:Zod v4 のベータ版では、
ZodErrorがformState.errorsに格納されず、直接スローされます。安定版の Zod v4.1.x 以降で修正済みです。ベータ版は避けてください -
大規模なフォームのパフォーマンス - #13129:resolver と formState の読み取りを伴う 300 個以上のフィールドでは、10~15 秒間フリーズします。4 つの回避策については、パフォーマンスのセクションを参照してください
-
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
破壊的変更:
- 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
- Watch コンポーネント:
names→name:
// V7:
<Watch names={["email", "password"]} />
// V8:
<Watch name={["email", "password"]} />
- 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]);
- 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] に更新