Claude
Skills
Sign in
Back

react-hook-form-zod

Included with Lifetime
$97 forever

Type-safe React forms with React Hook Form and Zod validation. Use for form schemas, field arrays, multi-step forms, or encountering validation errors, resolver issues, nested field problems.

Web Devscripts

What this skill does

# React Hook Form + Zod Validation

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

---

## Quick Start (10 Minutes)

### 1. Install Packages

```bash
bun add [email protected] [email protected] @hookform/[email protected]
```

**Why These Packages**:
- **react-hook-form**: Performant, flexible forms with minimal re-renders
- **zod**: TypeScript-first schema validation with type inference
- **@hookform/resolvers**: Adapter connecting Zod 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)
  }

  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)

**Template**: See `templates/basic-form.tsx` for complete working example

### 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 {
    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: 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

**Template**: See `templates/server-validation.ts`

---

## Core Concepts

### useForm Hook

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

**Validation Modes**:
- `onSubmit` - Validate on submit (best performance)
- `onChange` - Validate on every change (live feedback)
- `onBlur` - Validate when field loses focus (good balance)
- `all` - Validate on submit, blur, and change

**Reference**: See `references/rhf-api-reference.md` for complete API

### Zod Schema Basics

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

// Basic types
const schema = z.object({
  email: z.string().email('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.string().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
})
```

**Reference**: See `references/zod-schemas-guide.md` for complete patterns

---

## Critical Rules

### Always Do

✅ **Always set `defaultValues`** - Prevents "uncontrolled to controlled" warnings
✅ **Use `zodResolver` for validation** - Connects Zod schemas to React Hook Form
✅ **Infer types from schema** - Use `z.infer<typeof schema>` for type safety
✅ **Validate on server too** - Client validation can be bypassed
✅ **Use `.register()` for native inputs** - Simple and performant
✅ **Use `Controller` for custom components** - For component libraries (MUI, Chakra, etc.)
✅ **Handle errors accessibly** - Use `role="alert"` for screen readers
✅ **Reset form after submission** - Use `reset()` to clear form state

**Form Patterns**: See `templates/` for:
- `basic-form.tsx` - Simple login/register forms
- `advanced-form.tsx` - Nested objects, arrays, dynamic fields
- `shadcn-form.tsx` - Integration with shadcn/ui
- `multi-step-form.tsx` - Wizard/stepper forms
- `async-validation.tsx` - Async field validation

### Never Do

❌ **Never skip `defaultValues`** - Causes "uncontrolled to controlled" errors
❌ **Never use only client validation** - Security vulnerability
❌ **Never mutate form values directly** - Use `setValue()` instead
❌ **Never ignore accessibility** - Always use proper labels and ARIA
❌ **Never forget to disable submit when `isSubmitting`** - Prevents double submissions

**Performance**: See `references/performance-optimization.md` for:
- When to use `mode: 'onBlur'` vs `'onChange'`
- `useWatch` vs `watch()`
- Re-render optimization strategies

**Accessibility**: See `references/accessibility.md` for:
- Proper label association
- Error announcement
- Focus management
- Keyboard navigation

---

## Top 5 Critical Errors

### Error #1: Uncontrolled to Controlled Warning ⚠️

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

**Cause**: Not setting `defaultValues`

**Solution:**
```typescript
// ❌ BAD
const form = useForm()

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

---

### Error #2: Zod v4 Type Inference Issues

**Error:** Type inference doesn't work correctly

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

**Source**: [GitHub Issue #13109](https://github.com/react-hook-form/react-hook-form/issues/13109)

---

### Error #3: Resolver Not Found

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

**Solution:**
```bash
# Install the resolvers package
bun add @hookform/[email protected]
```

---

### Error #4: Arra

Related in Web Dev