mcpField
R
react-hook-form-zod

react-hook-form-zod

github.com/secondsky開発242386 回インストール5 週間前に更新
リポジトリ

react-hook-form-zod について

React Hook Form と Zod を使うフォーム開発者向けのガイドです。型安全な検証パターン、テンプレート、トラブルシューティングを提供し、クライアントとサーバー双方の検証を扱います。

主な機能

  1. 01React Hook Form と Zod の設定
  2. 02スキーマによる型推論
  3. 03動的フォームと複数ステップ形式
  4. 04非同期・サーバー側検証
  5. 05アクセシビリティと性能の指針
  6. 06テンプレートと問題解決資料

活用シーン

  1. 01ログイン・登録フォームの実装
  2. 02動的フィールドや複数ステップ形式の追加
  3. 03クライアントとサーバーでの入力検証
  4. 04Resolver や型エラーの調査

インストール

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

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

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

同梱ファイル

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

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
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.tsx
shadcn-form.tsx

詳細

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

ステータス:本番環境対応済み ✅ 最終更新日:2025-11-21 依存関係:なし(単体で動作) 最新バージョン:[email protected]、[email protected]、@hookform/[email protected]


クイックスタート(10 分)

1. パッケージをインストールする
bun add [email protected] [email protected] @hookform/[email protected]

これらのパッケージを使用する理由:

  • react-hook-form:再レンダリングを最小限に抑えた、高速で柔軟なフォーム
  • zod:型推論に対応した、TypeScript を最優先に設計されたスキーマバリデーション
  • @hookform/resolvers:Zod と React Hook Form を接続するアダプター
2. 最初のフォームを作成する
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'

// 1. Define validation schema
const loginSchema = z.object({
  email: z.email({ error: 'Invalid email address' }),
  password: z.string().min(8, 'Password must be at least 8 characters'),
})

// 2. Infer TypeScript type from schema
type LoginFormData = z.infer<typeof loginSchema>

function LoginForm() {
  // 3. Initialize form with zodResolver
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<LoginFormData>({
    resolver: zodResolver(loginSchema),
    defaultValues: {
      email: '',
      password: '',
    },
  })

  // 4. Handle form submission
  const onSubmit = async (data: LoginFormData) => {
    // Data is guaranteed to be valid here
    console.log('Valid data:', data)
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label htmlFor="email">Email</label>
        <input id="email" type="email" {...register('email')} />
        {errors.email && (
          <span role="alert" className="error">
            {errors.email.message}
          </span>
        )}
      </div>

      <div>
        <label htmlFor="password">Password</label>
        <input id="password" type="password" {...register('password')} />
        {errors.password && (
          <span role="alert" className="error">
            {errors.password.message}
          </span>
        )}
      </div>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Logging in...' : 'Login'}
      </button>
    </form>
  )
}

重要:

  • 「uncontrolled から controlled への切り替え」警告を防ぐため、常に defaultValues を設定してください
  • Zod のバリデーションを接続するには、zodResolver(schema) を使用してください
  • 完全な型安全性を確保するため、フォームの型には z.infer<typeof schema> を使用してください
  • クライアントとサーバーの両方でバリデーションしてください(クライアント側のバリデーションだけを信用しないでください)

テンプレート:動作する完全なサンプルについては、templates/basic-form.tsx を参照してください

3. サーバー側のバリデーションを追加する
// server/api/login.ts
import { z } from 'zod'

// SAME schema on server
const loginSchema = z.object({
  email: z.email({ error: 'Invalid email address' }),
  password: z.string().min(8, 'Password must be at least 8 characters'),
})

export async function loginHandler(req: Request) {
  try {
    const data = loginSchema.parse(await req.json())
    // Data is type-safe and validated
    return { success: true }
  } catch (error) {
    if (error instanceof z.ZodError) {
      return { success: false, errors: z.flattenError(error).fieldErrors }
    }
    throw error
  }
}

サーバー側でバリデーションする理由:

  • クライアント側のバリデーションは回避できます(要素の検証、Postman、curl など)
  • サーバー側のバリデーションはセキュリティレイヤーです
  • 同じ Zod スキーマを使うことで、信頼できる唯一の情報源になります

テンプレート:templates/server-validation.tsx を参照してください


コアコンセプト

useForm フック
const {
  register,           // Register input fields
  handleSubmit,       // Wrap onSubmit handler
  formState,          // Form state (errors, isValid, isDirty, etc.)
  setValue,           // Set field value programmatically
  getValues,          // Get current form values
  watch,              // Watch field values
  reset,              // Reset form to defaults
  trigger,            // Trigger validation manually
  control,            // For Controller/useController
} = useForm<FormData>({
  resolver: zodResolver(schema),
  mode: 'onSubmit',               // When to validate
  defaultValues: {},              // Initial values (REQUIRED)
})

バリデーションモード:

  • onSubmit - 送信時にバリデーションします(最高のパフォーマンス)
  • onChange - 変更のたびにバリデーションします(リアルタイムのフィードバック)
  • onBlur - フィールドからフォーカスが外れたときにバリデーションします(バランスに優れています)
  • all - 送信時、フォーカスが外れたとき、変更時にバリデーションします

リファレンス:API の全容については、references/rhf-api-reference.md を参照してください

Zod スキーマの基本
import { z } from 'zod'

// Basic types
const schema = z.object({
  email: z.email({ error: 'Invalid email' }),
  age: z.number().min(18, 'Must be 18+'),
  terms: z.boolean().refine(val => val === true, 'Must accept terms'),
})

// Nested objects
const addressSchema = z.object({
  user: z.object({
    name: z.string(),
    email: z.email(),
  }),
  address: z.object({
    street: z.string(),
    city: z.string(),
    zip: z.string().regex(/^\d{5}$/),
  }),
})

// Arrays
const tagsSchema = z.object({
  tags: z.array(z.string()).min(1, 'At least one tag required'),
})

// Optional and nullable
const optionalSchema = z.object({
  middleName: z.string().optional(),
  nickname: z.string().nullable(),
  bio: z.string().nullish(), // optional AND nullable
})

リファレンス:パターンの全容については、references/zod-schemas-guide.md を参照してください


重要なルール

必ず行うこと

✅ 常に defaultValues を設定する - 「uncontrolled から controlled への切り替え」警告を防ぎます ✅ バリデーションに zodResolver を使用する - Zod スキーマを React Hook Form に接続します ✅ スキーマから型を推論する - 型安全性を確保するため、z.infer<typeof schema> を使用します ✅ サーバー側でもバリデーションする - クライアント側のバリデーションは回避される可能性があります ✅ ネイティブ入力には .register() を使用する - シンプルで高パフォーマンスです ✅ カスタムコンポーネントには Controller を使用する - コンポーネントライブラリ(MUI、Chakra など)向けです ✅ アクセシビリティに配慮してエラーを処理する - スクリーンリーダー向けに role="alert" を使用します ✅ 送信後にフォームをリセットする - reset() を使ってフォームの状態をクリアします

フォームパターン:以下については templates/ を参照してください。

  • basic-form.tsx - シンプルなログイン/登録フォーム
  • advanced-form.tsx - ネストされたオブジェクト、配列、動的フィールド
  • shadcn-form.tsx - shadcn/ui との統合
  • multi-step-form.tsx - ウィザード/ステッパー形式のフォーム
  • async-validation.tsx - 非同期フィールドバリデーション
絶対に行わないこと

❌ defaultValues を省略しない - 「uncontrolled から controlled への切り替え」エラーの原因になります ❌ クライアント側だけでバリデーションしない - セキュリティ上の脆弱性につながります ❌ フォームの値を直接変更しない - 代わりに setValue() を使用してください ❌ アクセシビリティを無視しない - 常に適切なラベルと ARIA を使用してください ❌ isSubmitting のときに送信を無効化し忘れない - 二重送信を防ぎます

パフォーマンス:以下については references/performance-optimization.md を参照してください。

  • mode: 'onBlur' と 'onChange' の使い分け
  • useWatch と watch() の使い分け
  • 再レンダリングの最適化戦略

アクセシビリティ:以下については references/accessibility.md を参照してください。

  • 適切なラベルの関連付け
  • エラーの通知
  • フォーカス管理
  • キーボード操作

特に重要なエラー 5 件

エラー #1:Uncontrolled から Controlled への切り替えに関する警告 ⚠️

エラー:

Warning: A component is changing an uncontrolled input to be controlled

原因:defaultValues が設定されていない

解決策:

// ❌ BAD
const form = useForm()

// ✅ GOOD
const form = useForm({
  defaultValues: {
    email: '',
    password: '',
  }
})

エラー #2:Zod v4 の型推論に関する問題

エラー: 型推論が正しく機能しない

解決策:

// Explicitly type useForm if needed
const form = useForm<z.infer<typeof schema>>({
  resolver: zodResolver(schema),
})

出典:GitHub Issue #13109


エラー #3:Resolver が見つからない

エラー:

Module not found: Can't resolve '@hookform/resolvers/zod'

解決策:

# Install the resolvers package
bun add @hookform/[email protected]

エラー #4:配列フィールドに関する問題

エラー: 動的な配列フィールドが useFieldArray で機能しない

解決策:

const { fields, append, remove } = useFieldArray({
  control,
  name: "items" // Must match schema field name exactly
})

テンプレート:templates/dynamic-fields.tsx を参照してください


エラー #5:カスタムコンポーネントのバリデーションに失敗する

エラー: サードパーティ製コンポーネント(MUI、Chakra)のバリデーションが機能しない

解決方法: register の代わりに Controller を使用します。

<Controller
  name="date"
  control={control}
  render={({ field }) => (
    <DatePicker {...field} />
  )}
/>

参考資料: すべてのパターンについては references/error-handling.md を参照してください。


全12件のエラー: 詳細なドキュメントについては references/top-errors.md を参照してください。


よく使われるパターン

基本的なフォーム
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'

const schema = z.object({
  name: z.string().min(1, 'Name required'),
  email: z.email({ error: 'Invalid email' }),
})

type FormData = z.infer<typeof schema>

function MyForm() {
  const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
    resolver: zodResolver(schema),
    defaultValues: { name: '', email: '' }
  })

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

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('name')} />
      {errors.name && <span>{errors.name.message}</span>}
      <button type="submit">Submit</button>
    </form>
  )
}

テンプレート: templates/basic-form.tsx を参照してください。


動的なフィールド(useFieldArray)
import { useForm, useFieldArray } from 'react-hook-form'

const schema = z.object({
  items: z.array(
    z.object({
      name: z.string(),
      quantity: z.number().min(1)
    })
  ).min(1, 'At least one item required')
})

function DynamicForm() {
  const { control, handleSubmit } = useForm({
    resolver: zodResolver(schema),
    defaultValues: { items: [{ name: '', quantity: 1 }] }
  })

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

  return (
    <form>
      {fields.map((field, index) => (
        <div key={field.id}>
          <input {...register(`items.${index}.name`)} />
          <button onClick={() => remove(index)}>Remove</button>
        </div>
      ))}
      <button onClick={() => append({ name: '', quantity: 1 })}>
        Add Item
      </button>
    </form>
  )
}

テンプレート: templates/dynamic-fields.tsx を参照してください。


非同期バリデーション
const schema = z.object({
  username: z.string()
    .min(3)
    .refine(async (username) => {
      const response = await fetch(`/api/check-username?username=${username}`)
      const { available } = await response.json()
      return available
    }, 'Username already taken')
})

テンプレート: templates/async-validation.tsx を参照してください。


複数ステップのフォーム
function MultiStepForm() {
  const [step, setStep] = useState(1)
  const form = useForm({
    resolver: zodResolver(schema),
    mode: 'onBlur' // Validate each step before proceeding
  })

  const onSubmit = async (data) => {
    if (step < 3) {
      setStep(step + 1)
    } else {
      // Final submission
      await submitForm(data)
    }
  }

  return (
    <form onSubmit={form.handleSubmit(onSubmit)}>
      {step === 1 && <Step1Fields />}
      {step === 2 && <Step2Fields />}
      {step === 3 && <Step3Fields />}
      <button type="submit">
        {step < 3 ? 'Next' : 'Submit'}
      </button>
    </form>
  )
}

テンプレート: templates/multi-step-form.tsx を参照してください。


shadcn/ui との統合

import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'

function ShadcnForm() {
  const form = useForm({
    resolver: zodResolver(schema),
    defaultValues: { email: '' }
  })

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)}>
        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Email</FormLabel>
              <FormControl>
                <Input {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
      </form>
    </Form>
  )
}

参考資料: すべてのパターンについては references/shadcn-integration.md を参照してください。 テンプレート: templates/shadcn-form.tsx を参照してください。


同梱リソースの使用

テンプレート(templates/)

コピーしてすぐに使える例:

  • basic-form.tsx - バリデーション付きのシンプルなログイン/登録フォーム
  • advanced-form.tsx - ネストされたオブジェクト、配列、条件付きフィールド
  • shadcn-form.tsx - shadcn/ui Form コンポーネントとの統合
  • multi-step-form.tsx - ステップごとのバリデーション付きウィザード/ステッパーフォーム
  • dynamic-fields.tsx - 動的なフォームフィールドに使う useFieldArray
  • async-validation.tsx - 非同期フィールドバリデーション(ユーザー名の確認など)
  • server-validation.tsx - Zod を使用したサーバー側バリデーション
  • custom-error-display.tsx - カスタムエラーメッセージコンポーネント
  • package.json - パッケージのバージョンとスクリプト
参考資料(references/)

詳細なドキュメント:

  • top-errors.md - よくある12件のエラーとその解決方法、情報源
  • rhf-api-reference.md - React Hook Form API の完全なリファレンス
  • zod-schemas-guide.md - Zod スキーマの包括的なパターン
  • shadcn-integration.md - shadcn/ui Form の統合ガイド
  • error-handling.md - エラー表示のパターンとアクセシビリティ
  • performance-optimization.md - 再レンダリングの最適化戦略
  • accessibility.md - WCAG 準拠とスクリーンリーダーのサポート
  • links-to-official-docs.md - 整理された公式ドキュメントへのリンク

参考資料を読み込むタイミング

参考資料読み込むタイミング
top-errors.mdバリデーションの問題、型エラー、「uncontrolled to controlled」警告をデバッグするとき
rhf-api-reference.mduseForm、register、Controller、formState の完全な API が必要なとき
zod-schemas-guide.md複雑なスキーマ(ネスト、配列、条件付き、非同期バリデーション)を作成するとき
shadcn-integration.mdshadcn/ui の Form、FormField、FormItem コンポーネントを使用するとき
error-handling.mdカスタムエラー表示、バリデーションのタイミング、エラーメッセージのパターンが必要なとき
performance-optimization.mdフォームの再レンダリングが多すぎるとき、watch/useWatch を最適化するとき
accessibility.mdWCAG 準拠、スクリーンリーダー、キーボードナビゲーションについて確認するとき
links-to-official-docs.md公式ドキュメントへのリンクが必要なとき

パフォーマンスのヒント

簡単なヒント:

  • UX とパフォーマンスのバランスを取るには mode: 'onBlur' を使用します。
  • 特定のフィールドには watch() の代わりに useWatch を使用します。
  • バリデーションスキーマはコンポーネントの外でメモ化します。
  • 条件付きフィールドには shouldUnregister: false を使用します。
  • 引数なしで watch() を使用するのは避けます(すべてのフィールドを監視します)。

参考資料: 詳細な戦略については references/performance-optimization.md を参照してください。


アクセシビリティ

簡単なチェックリスト:

  • ✅ すべての入力に <label htmlFor="fieldId"> を使用します。
  • ✅ エラーメッセージに role="alert" を追加します。
  • ✅ 無効なフィールドに aria-invalid="true" を使用します。
  • ✅ キーボードナビゲーション(Tab、Enter、Escape)が機能することを確認します。
  • ✅ 明確で、対応方法がわかるエラーメッセージを提示します。

参考資料: WCAG 準拠ガイドについては references/accessibility.md を参照してください。


バリデーションスキーマ(Zod)

よく使われるパターン:

// Email
z.email({ error: 'Invalid email' })

// Password (min 8 chars, 1 uppercase, 1 number)
z.string()
  .min(8)
  .regex(/[A-Z]/, 'Need uppercase')
  .regex(/[0-9]/, 'Need number')

// URL
z.url({ error: 'Invalid URL' })

// Date
z.datetime() // ISO 8601
z.date() // JS Date object

// File upload
z.instanceof(File)
  .refine(file => file.size <= 5000000, 'Max 5MB')
  .refine(
    file => ['image/jpeg', 'image/png'].includes(file.type),
    'Only JPEG/PNG allowed'
  )

// Custom validation
z.string().refine(
  val => val !== 'admin',
  'Username "admin" is reserved'
)

// Async validation
z.string().refine(
  async (username) => {
    const available = await checkUsername(username)
    return available
  },
  'Username already taken'
)

参考資料: すべてのパターンについては references/zod-schemas-guide.md を参照してください。


依存関係

必須:

任意:

  • @radix-ui/react-label@latest - shadcn/ui との統合用
  • class-variance-authority@latest - shadcn/ui のスタイリング用

公式ドキュメント

参考:リンクをまとめた一覧は references/links-to-official-docs.md を参照してください。


トラブルシューティング

「非制御コンポーネントから制御コンポーネントへの変更」警告

解決策:必ず defaultValues を設定してください → references/top-errors.md #2 を参照

Zod v4 での型推論の問題

解決策:useForm<z.infer<typeof schema>> の型を明示的に指定してください → references/top-errors.md #1 を参照

Resolver が見つからないエラー

解決策:@hookform/resolvers パッケージをインストールしてください → references/top-errors.md #3 を参照

カスタムコンポーネントが検証されない

解決策:register ではなく Controller を使用してください → references/top-errors.md #5 を参照

フォームの再レンダリングが多すぎる

解決策:mode: 'onBlur' と useWatch を使用してください → references/performance-optimization.md を参照


本番環境での使用例

このスキルは、本番環境での次のようなパターンに基づいています。

  • 実際のフォーム:ログイン、登録、チェックアウト、複数ステップのウィザード
  • 検証:共有 Zod スキーマを使ったクライアントとサーバーでの検証
  • アクセシビリティ:WCAG 2.1 AA に準拠
  • パフォーマンス:再レンダリングを最小限に抑えるよう最適化

トークン節約:約60%(テンプレートを含む包括的なフォームパターン) エラー防止:100%(解決策を含む、文書化された12件すべての問題) 本番環境ですぐに使用可能! ✅

コメント