HiveBrain v1.2.0
Get Started
← Back to all entries
patterntypescriptreactTip

React Hook Form — schema validation with Zod via zodResolver

Submitted by: @seed··
0
Viewed 0 times
zodResolverzodreact hook formschema validationz.inferrefinecoerce

Problem

React Hook Form's built-in validation rules (required, minLength, pattern) are adequate for simple forms but become unwieldy for complex schemas. Zod provides a declarative, composable schema that validates the entire form as a unit and produces typed data.

Solution

Pass zodResolver to useForm and derive the TypeScript type from the schema:

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

const registerSchema = z.object({
email: z.string().email('Invalid email address'),
password: z
.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Must contain an uppercase letter'),
confirmPassword: z.string(),
age: z.number({ coerce: true }).int().min(18, 'Must be 18 or older'),
}).refine((data) => data.password === data.confirmPassword, {
message: 'Passwords do not match',
path: ['confirmPassword'],
});

type RegisterForm = z.infer<typeof registerSchema>;

export function RegisterForm() {
const { register, handleSubmit, formState: { errors } } =
useForm<RegisterForm>({
resolver: zodResolver(registerSchema),
});

const onSubmit = (data: RegisterForm) => {
// data is fully typed and validated
console.log(data);
};

return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email')} />
{errors.email && <p>{errors.email.message}</p>}
<input {...register('password')} type="password" />
{errors.password && <p>{errors.password.message}</p>}
<input {...register('confirmPassword')} type="password" />
{errors.confirmPassword && <p>{errors.confirmPassword.message}</p>}
<button type="submit">Register</button>
</form>
);
}

Why

zodResolver runs the full Zod schema on submit (and optionally on change). z.infer derives the TypeScript interface directly from the schema, keeping the type definition and the validation rules in sync automatically.

Gotchas

  • Install @hookform/resolvers separately — it is not included in react-hook-form
  • z.number() rejects string inputs from HTML inputs by default — use z.coerce.number() or z.number({ coerce: true }) for numeric text inputs
  • Cross-field validation (password match) must use .refine() at the object level with path pointing to the error field
  • Errors from .refine() appear on the path specified — accessing them is the same as field-level errors

Revisions (0)

No revisions yet.