React Hook Form and Zod: My Form Stack
September 24, 2026 Avishka Devinda
September 24, 2026 Avishka Devinda
Forms look simple until they are not.
A login form may only have two fields, but a real application quickly adds validation, loading states, server errors, optional fields, nested data, and reusable input components.
For most React and Next.js projects, I like using React Hook Form with Zod.
The combination gives me a clear separation:
React Hook Form keeps form state lightweight and gives me a good API for registering inputs, handling submission, and displaying validation errors.
A small example:
const form = useForm<FormValues>();
<form onSubmit={form.handleSubmit(onSubmit)}>
<input {...form.register('email')} />
<button type="submit">Continue</button>
</form>
I do not need to manually create state for every input.
That becomes much nicer when the form has ten or twenty fields.
Zod lets me define validation as a schema.
const formSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
message: z.string().min(10),
});
The schema is easy to read, but the bigger advantage is that I can reuse the same rules outside the form.
For example, I can validate again on the server before writing data to a database or sending an email.
Client-side validation improves user experience.
Server-side validation protects the application.
I want both.
With the Zod resolver, the schema becomes part of React Hook Form:
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
name: '',
email: '',
message: '',
},
});
Now the form values and validation rules come from the same schema.
That reduces duplicated types.
One mistake I try to avoid is assuming that React validation is security.
Anyone can bypass the browser UI and send a request directly.
So I validate again in the server action or route handler:
const result = formSchema.safeParse(body);
if (!result.success) {
return {
success: false,
error: 'Invalid form data',
};
}
Only after validation succeeds do I use the data.
Not every error belongs in Zod.
A field can be valid but still fail on the server.
Examples:
I normally return these errors from the server and map them back into the form.
That keeps schema validation focused on the shape and rules of the data.
Zod is powerful, but a schema can become difficult to understand if every business rule is pushed into one giant validation object.
I prefer small schemas with clear responsibilities.
For example:
const emailSchema = z.string().email();
const profileSchema = z.object({
displayName: z.string().min(2).max(50),
bio: z.string().max(160).optional(),
});
Small schemas are easier to test and reuse.
This stack also works nicely with component systems such as shadcn/ui.
I can keep validation logic separate from presentation.
The input component does not need to know how the entire form works. It only needs the field props and error state.
That makes it easier to keep the UI consistent across login, settings, onboarding, and dashboard forms.
Before I call a form finished, I normally check:
None of these are exciting features, but they make a big difference in production.
React Hook Form and Zod solve different parts of the same problem.
React Hook Form makes the interaction easy to manage.
Zod makes the data contract explicit.
Together, they give me a form architecture that stays simple even when the UI becomes more complicated.