React: From Components to Production · Forms and Validation
Complex Forms with React Hook Form and Zod
Validate with a schema shared between client and server.
For large forms with complex validation, React Hook Form keeps inputs mostly uncontrolled (few re-renders) and manages errors, dirty state and submission. Pair it with Zod schemas via @hookform/resolvers so types and validation rules come from one source, which can also validate on the server.
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
const schema = z.object({
name: z.string().min(2, "At least 2 characters"),
email: z.string().email("Invalid email"),
age: z.coerce.number().int().min(18, "Must be 18+"),
});
type FormValues = z.infer<typeof schema>;
export function ProfileForm({ onSave }: { onSave: (v: FormValues) => Promise<void> }) {
const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<FormValues>({
resolver: zodResolver(schema),
});
return (
<form onSubmit={handleSubmit(onSave)} noValidate>
<label>Name <input {...register("name")} aria-invalid={!!errors.name} /></label>
{errors.name && <p role="alert">{errors.name.message}</p>}
<label>Email <input type="email" {...register("email")} aria-invalid={!!errors.email} /></label>
{errors.email && <p role="alert">{errors.email.message}</p>}
<label>Age <input type="number" {...register("age")} /></label>
{errors.age && <p role="alert">{errors.age.message}</p>}
<button disabled={isSubmitting}>Save</button>
</form>
);
}
Common pitfalls
- Duplicating validation rules between client and server instead of sharing schemas.
- Not mapping server-side errors back to fields (
setError). - Forgetting
z.coercefor numeric inputs (HTML inputs return strings).
Key points
- React Hook Form minimizes re-renders
- Zod gives types + validation
- Always validate again on the server
Further reading: React Hook Form: Get started
Practice questions
Why is React Hook Form efficient for big forms?
Answer
It registers mostly uncontrolled inputs and reads values via refs, so typing doesn't re-render the whole form; only components subscribed to specific state update.
What's the benefit of z.infer<typeof schema>?
Answer
The TypeScript type for form values is derived from the validation schema, so types and validation rules can't drift apart.
Open in the app to mark lessons complete, take notes and track your progress.