Back to Blog
Tutorial 9 min read

Mastering React Hook Form + Zod + Forge in 2026

S
Sarah ChenStaff Engineer, Forge · Published February 28, 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: false on 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