Claude
Skills
Sign in
Back

react-hook-form-zod

Included with Lifetime
$97 forever

This skill provides comprehensive knowledge for building type-safe, validated forms in React using React Hook Form and Zod schema validation. Use when: building forms with validation in React, integrating Zod schema validation with React Hook Form, using shadcn/ui Form or Field components, implementing client and server-side validation with a single schema, handling complex validation scenarios (nested objects, arrays, conditional fields, async validation), building multi-step forms or wizards, implementing dynamic form fields with useFieldArray, optimizing form performance and re-renders, ensuring accessible form error handling, or debugging form validation issues. Keywords: react-hook-form, useForm, zod validation, zodResolver, @hookform/resolvers, form schema, register, handleSubmit, formState, useFieldArray, useWatch, useController, Controller, shadcn form, Field component, client server validation, nested validation, array field validation, dynamic fields, multi-step form, async validation, zod refine, z.infer, form error handling, uncontrolled to controlled, resolver not found, schema validation error

Design

What this skill does


# React Hook Form + Zod Validation

**Status**: Production Ready ✅
**Last Updated**: 2025-10-23
**Dependencies**: None (standalone)
**Latest Versions**: [email protected], [email protected], @hookform/[email protected]

---

## Quick Start (10 Minutes)

### 1. Install Packages

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

**Why These Packages**:
- **react-hook-form**: Performant, flexible form library with minimal re-renders
- **zod**: TypeScript-first schema validation with type inference
- **@hookform/resolvers**: Adapter to connect Zod (and other validators) to React Hook Form

### 2. Create Your First Form

```typescript
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.string().email('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)
    // Make API call, etc.
  }

  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>
  )
}
```

**CRITICAL**:
- Always set `defaultValues` to prevent "uncontrolled to controlled" warnings
- Use `zodResolver(schema)` to connect Zod validation
- Type form with `z.infer<typeof schema>` for full type safety
- Validate on both client AND server (never trust client validation alone)

### 3. Add Server-Side Validation

```typescript
// server/api/login.ts
import { z } from 'zod'

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

export async function loginHandler(req: Request) {
  try {
    // Parse and validate request body
    const data = loginSchema.parse(await req.json())

    // Data is type-safe and validated
    // Proceed with authentication logic
    return { success: true }
  } catch (error) {
    if (error instanceof z.ZodError) {
      // Return validation errors to client
      return { success: false, errors: error.flatten().fieldErrors }
    }
    throw error
  }
}
```

**Why Server Validation**:
- Client validation can be bypassed (inspect element, Postman, curl)
- Server validation is your security layer
- Same Zod schema = single source of truth
- Type safety across frontend and backend

---

## Core Concepts

### useForm Hook Anatomy

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

**useForm Options**:

| Option | Description | Default |
|--------|-------------|---------|
| `resolver` | Validation resolver (e.g., zodResolver) | undefined |
| `mode` | When to validate ('onSubmit', 'onChange', 'onBlur', 'all') | 'onSubmit' |
| `reValidateMode` | When to re-validate after error | 'onChange' |
| `defaultValues` | Initial form values | {} |
| `shouldUnregister` | Unregister inputs when unmounted | false |
| `criteriaMode` | Return all errors or first error only | 'firstError' |

**Form Validation Modes**:
- `onSubmit` - Validate on submit (best performance, less responsive)
- `onChange` - Validate on every change (live feedback, more re-renders)
- `onBlur` - Validate when field loses focus (good balance)
- `all` - Validate on submit, blur, and change (most responsive, highest cost)

### Zod Schema Definition

```typescript
import { z } from 'zod'

// Primitives
const stringSchema = z.string()
const numberSchema = z.number()
const booleanSchema = z.boolean()
const dateSchema = z.date()

// With validation
const emailSchema = z.string().email('Invalid email')
const ageSchema = z.number().min(18, 'Must be 18+').max(120, 'Invalid age')
const usernameSchema = z.string().min(3).max(20).regex(/^[a-zA-Z0-9_]+$/)

// Objects
const userSchema = z.object({
  name: z.string(),
  email: z.string().email(),
  age: z.number().int().positive(),
})

// Arrays
const tagsSchema = z.array(z.string())
const usersSchema = z.array(userSchema)

// Optional and Nullable
const optionalField = z.string().optional()       // string | undefined
const nullableField = z.string().nullable()       // string | null
const nullishField = z.string().nullish()         // string | null | undefined

// Default values
const withDefault = z.string().default('default value')

// Unions
const statusSchema = z.union([
  z.literal('active'),
  z.literal('inactive'),
  z.literal('pending'),
])
// Shorthand for literals
const statusEnum = z.enum(['active', 'inactive', 'pending'])

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

const profileSchema = z.object({
  name: z.string(),
  address: addressSchema,  // Nested object
})

// Custom error messages
const passwordSchema = z.string()
  .min(8, { message: 'Password must be at least 8 characters' })
  .regex(/[A-Z]/, { message: 'Password must contain uppercase letter' })
  .regex(/[0-9]/, { message: 'Password must contain number' })
```

**Type Inference**:
```typescript
const userSchema = z.object({
  name: z.string(),
  age: z.number(),
})

// Automatically infer TypeScript type
type User = z.infer<typeof userSchema>
// Result: { name: string; age: number }
```

### Zod Refinements (Custom Validation)

```typescript
// Simple refinement
const passwordConfirmSchema = z.object({
  password: z.string().min(8),
  confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
  message: "Passwords don't match",
  path: ['confirmPassword'], // Error will appear on confirmPassword field
})

// Multiple refinements
const signupSchema = z.object({
  username: z.string(),
  email: z.string().email(),
  age: z.number(),
})
  .refine((data) => data.username !== data.email.split('@')[0], {
    message: 'Username cannot be your email prefix',
    path: ['username'],
  })
  .refine((data) => data.age >= 18, {
    message: 'Must be 18 or older',
    path: ['age'],
  })

// Async refinement (for API checks)
const usernameSchema = z.string().refine(async (username) => {
  // Check if username is available via API
 
Files: 4
Size: 125.2 KB
Complexity: 49/100
Category: Design

Related in Design