GitHub

Form Validation with Zod

Schema-first form validation with React Hook Form and Zod. Type-safe, declarative error messages, and zero boilerplate for create and edit flows.

01

Principle

The Zod schema is the single source of truth — it defines the shape, types, and error messages. React Hook Form handles registration, submission, and field state. Components never write validation logic; they display what the schema declares.

lightbulb

Write the schema before a single input. Share schemas across forms with .pick(), .extend(), or .omit(). Keep all error messages inside the schema, not in JSX.

02

Rules

  • check_circle
    Schema before formDefine the Zod schema first. Never add validation inline with register options or manual if-statements.
  • check_circle
    Omit server-generated fieldsUse .omit({ id: true, createdAt: true }) for create forms. The schema reflects what the user provides.
  • check_circle
    handleSubmit owns errorsWrap mutation calls in handleSubmit. Validation errors surface automatically without try/catch in the component.
  • check_circle
    Reset after successCall reset() after a successful mutation to clear all field values and dirty state.
03

Pattern

components/UserForm.tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const createUserSchema = z.object({
  name:   z.string().min(1, 'Name is required'),
  email:  z.string().email('Enter a valid email address'),
  role:   z.enum(['viewer', 'editor', 'admin']),
  status: z.enum(['active', 'inactive']),
});

type CreateUserValues = z.infer<typeof createUserSchema>;

export function UserForm() {
  const { register, handleSubmit, reset,
    formState: { errors, isSubmitting } } = useForm<CreateUserValues>({
    resolver: zodResolver(createUserSchema),
    defaultValues: { name: '', email: '', role: 'viewer', status: 'active' },
  });

  const onSubmit = async (data: CreateUserValues) => {
    await createUser(data);
    reset();
  };

  return <form onSubmit={handleSubmit(onSubmit)}>{/* fields */}</form>;
}
04

Implementation

info

Version Compatibility

Requires React 19+ and the latest stable versions of all dependencies shown.

In Next.js App Router, pair the form with a Server Action for zero-client-bundle mutations. Validate with the same Zod schema on the server to prevent bypassing client validation.

app/users/actions.ts
'use server';

import { createUserSchema } from '@/lib/schemas';
import { db } from '@/lib/db';

export async function createUserAction(values: unknown) {
  const data = createUserSchema.parse(values); // validates server-side too
  await db.user.create({ data });
}
05

Live Demo

menu_book
React Patterns

Helping developers build robust React applications since 2025.

© 2025 React Patterns Cookbook. Built with ❤️ for the community.
react-principles