Mastering React Hook Form + Zod + Forge in 2026
React Hook Form and Zod are the gold standard for client-side form validation. Combined with the Forge backend, you get a complete form stack: type-safe validation, optimistic UI, and secure server-side persistence — with zero boilerplate.
Why This Stack?
React Hook Form is uncontrolled by default, which means it doesn't re-render your component on every keystroke. For forms with 10+ fields, this translates to measurably smoother UX. Zod provides TypeScript-first schema validation that works identically on the client and server. Forge closes the loop by accepting that same Zod schema as its server-side validation config.
Setting Up the Schema
import { z } from "zod";
export const contactSchema = z.object({
firstName: z.string().min(1, "First name is required"),
lastName: z.string().min(1, "Last name is required"),
workEmail: z.string().email("Must be a valid work email"),
company: z.string().min(2, "Company name too short"),
companySize: z.enum(["1-10", "11-50", "51-200", "201-1000", "1000+"]),
message: z.string().min(10).max(1000),
});
export type ContactFormData = z.infer<typeof contactSchema>;Wiring Up React Hook Form
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { contactSchema, ContactFormData } from "./schema";
export function ContactForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting, isSubmitSuccessful },
} = useForm<ContactFormData>({
resolver: zodResolver(contactSchema),
defaultValues: { companySize: "11-50" },
});
const onSubmit = async (data: ContactFormData) => {
const res = await fetch(
`https://api.useforge.cloud/f/YOUR_FORM_ID/submit`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
}
);
if (!res.ok) throw new Error("Submission failed");
};
if (isSubmitSuccessful) {
return <SuccessScreen />;
}
return (
<form onSubmit={handleSubmit(onSubmit)} noValidate>
{/* Fields */}
</form>
);
}Handling Errors Gracefully
The formState.errors object gives you field-level error messages from Zod. Display them inline rather than in a toast — users need to see which field failed.
<div className="field">
<input
{...register("workEmail")}
type="email"
className={errors.workEmail ? "border-red-500" : "border-zinc-300"}
/>
{errors.workEmail && (
<p className="text-red-500 text-sm mt-1">
{errors.workEmail.message}
</p>
)}
</div>Multi-Step Forms
For multi-step forms, use trigger() to validate only the current step's fields before advancing. Submit to Forge only on the final step.
Our data shows multi-step forms with 3–4 steps convert 2.3× better than single-page forms for B2B use cases, primarily because they reduce cognitive load by presenting one decision at a time.
Performance Tips
- Use
mode: "onBlur"for validation timing — not "onChange" - Lazy-import heavy field components (file upload, date picker) with next/dynamic
- Debounce async validation (e.g. email availability checks) at least 400ms
- Set
shouldUnregister: falseon multi-step forms to preserve values
See the full Forge React docs for complete examples including file upload, CAPTCHA integration, and multi-step patterns. Start free at useforge.cloud.
Ready to build?
Create your free Forge account. 4 forms, 100 submissions/month, forever free.
Start Free