Skip to content

React

React Hook Form + Zod: Forms That Validate Themselves

No Duplicate Validation Logic

If you've been building forms in React the traditional way — hand-written validation, error state management, manual field sync — you're doing 3x the work. React Hook Form + Zod fixes this in one pattern: define a schema once, get full type safety on both sides of the wire, and let the form validate itself. The setup is deceptively simple. Define a Zod schema for your data shape. Wire it to React Hook Form's `useForm` hook via the `zodResolver`. The form now knows what fields exist, what types they should be, and what rules apply. Submit validation happens automatically. Errors render based on the same schema. If you change the schema, your form changes everywhere. Here's the pattern: ```typescript import { z } from 'zod'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; const ContactSchema = z.object({ email: z.string().email('Invalid email'), message: z.string().min(10, 'At least 10 chars'), subscribe: z.boolean().default(false), }); type Contact = z.infer; export function ContactForm() { const { register, handleSubmit, formState: { errors } } = useForm({ resolver: zodResolver(ContactSchema), }); return (
{ // data is Contact type — 100% type-safe const validated = await ContactSchema.parseAsync(data); await fetch('/api/contact', { method: 'POST', body: JSON.stringify(validated) }); })}> {errors.email && {errors.email.message}}
); } ``` The money part: on the server side, you don't write new validation. You use the same schema. A Supabase Edge Function or a Next.js Route Handler just runs `ContactSchema.parse()` on the request body. If the client sent garbage, the server rejects it. If someone bypassed the client validation entirely, the schema still catches it. One source of truth. The catch is that Zod adds runtime overhead. If your form has 50 fields, parsing all of them on every keystroke costs CPU cycles. The solution is simple: validate on blur, not onChange. React Hook Form supports `mode: 'onBlur'` to run validation only when the field loses focus. For most forms, this is the right trade-off. The other thing people get wrong: they define the schema inside the component. Move it outside. Keep your schema in a shared types file, import it in the component, import it again on the server. That's your single source of truth — not code duplication, actual data truth. React Hook Form + Zod is the 2025 standard for building forms in React. If you're still managing validation state by hand, you've already lost. The boilerplate savings alone justify it — but the type safety and server validation guarantee are why you should actually care.
Let us make some quick suggestions?
Please provide your full name.
Please provide your phone number.
Please provide a valid phone number.
Please provide your email address.
Please provide a valid email address.
Please provide your brand name or website.
Please provide your brand name or website.