Receber por email
← Voltar para o catálogo

react-hook-form-zod

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.

178estrelas
Atualizado há 5 meses

Ver no GitHub ↗ Licença: MIT

Como adicionar

/plugin marketplace add secondsky/claude-skills

O comando exato pode variar conforme o repositório. Confira o README no GitHub.

Para o autor da skill

Cole no README do seu repo

Mostra que sua skill está catalogada na Skillteca, gera backlink e tráfego rastreável.

[![Skillteca](https://www.skillteca.com.br/api/badge/react-hook-form-zod-secondsky/svg)](https://www.skillteca.com.br/skills/react-hook-form-zod-secondsky)

Alerta por categoria

Receba novas skills de Design e Frontend toda segunda

1 email curto, só com as skills novas de Design e Frontend. 4 minutos de leitura, sem spam, cancela com 1 clique.

Você confirma o email no primeiro envio. Sem spam. Cancela com 1 clique.

Ler descrição completa

React Hook Form + Zod Validation

Status: Production Ready ✅ Last Updated: 2025-11-21 Dependencies: None (standalone) Latest Versions: react-hook-form@7.66.1, zod@4.1.12, @hookform/resolvers@5.2.2

Quick Start (10 Minutes)

1. Install Packages

bun add react-hook-form@7.66.1 zod@4.1.12 @hookform/resolvers@5.2.2

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

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

// 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

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

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:

// ❌ 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:

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

Source: GitHub Issue #13109

Error #3: Resolver Not Found

Error:

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

Solution:

# Install the resolvers package
bun add @hookform/resolvers@5.2.2

Error #4: Arra

Compartilhar X LinkedIn