Welcome to the ultimate teaching guide for modern form management in React! This README is designed to help students understand how to build performant, type-safe, and scalable forms using two of the most popular libraries in the ecosystem: React Hook Form (RHF) and Zod.
In modern web development, managing forms can be a nightmare. React Hook Form and Zod solve this by dividing the responsibility:
- React Hook Form (The Engine): Handles the form state, submission, and UI updates. Itβs ultra-fast because it minimizes unnecessary re-renders.
- Zod (The Brain/Validator): Defines the "rules" (schema) for your data. It ensures that the inputs are valid before they ever reach your backend.
- β Type Safety: Automatically infer TypeScript types from your schema.
- β Performance: No more lagging UI on every keystroke.
- β Cleaner Code: All validation logic stays in one place, away from your UI components.
To get started, you'll need the following packages:
pnpm i react-hook-form zod @hookform/resolversreact-hook-form: Core library.zod: Schema declaration and validation.@hookform/resolvers: The "bridge" that connects them.
Think of the schema as the "blueprint" of your data.
import { z } from 'zod';
const registrationSchema = z.object({
username: z.string().min(3, "Username must be at least 3 letters"),
email: z.string().email("Please enter a valid email address"),
password: z.string().min(8, "Password must be at least 8 characters long"),
age: z.number({ invalid_type_error: "Age is required" }).min(18, "Must be at least 18"),
});You don't need to write manual interfaces anymore! Zod does it for you.
type RegistrationData = z.infer<typeof registrationSchema>;Connect the Zod schema to React Hook Form using the zodResolver.
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
// ... inside your component
const {
register,
handleSubmit,
formState: { errors },
} = useForm<RegistrationData>({
resolver: zodResolver(registrationSchema),
defaultValues: {
username: '',
email: '',
password: '',
}
});Connect your inputs using the register function.
const onSubmit = (data: RegistrationData) => {
console.log("Success! Data is clean:", data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input {...register("username")} placeholder="Username" />
{errors.username && <span>{errors.username.message}</span>}
</div>
<div>
<input type="email" {...register("email")} placeholder="Email" />
{errors.email && <span>{errors.email.message}</span>}
</div>
<div>
<input type="number" {...register("age", { valueAsNumber: true })} placeholder="Age" />
{errors.age && <span>{errors.age.message}</span>}
</div>
<button type="submit">Sign Up</button>
</form>
);Standard HTML input tags always return values as strings, even if the type is number. Use { valueAsNumber: true } in your register call to ensure Zod receives a actual number for validation.
You can define your Zod schemas in a separate file and share them between your Frontend and Backend. This ensures your data rules are consistent across your entire application.
Keep an eye on formState.isSubmitting and formState.isValid. These flags are incredibly helpful for disabling buttons or showing loading states!
- Install the 3 core packages.
- Write a
z.objectschema. - Infer the TypeScript type with
z.infer. - Pass
zodResolver(schema)to theuseFormhook. - Register inputs and display error messages.
Happy Coding! β¨