splitforms.com
guide · form validation

React Form Validation — Working Code (React Hook Form + Zod)

Modern React form validation uses React Hook Form for state management and Zod for typed schemas. Working code with inline errors, password confirmation, async submission to splitforms.

tsx
// app/contact-form.tsx
"use client";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";

const schema = z.object({
  name: z.string().min(2, "Name must be at least 2 characters"),
  email: z.string().email("Invalid email"),
  password: z.string().min(8, "Password must be at least 8 characters"),
  password_confirm: z.string(),
}).refine((d) => d.password === d.password_confirm, {
  message: "Passwords don't match",
  path: ["password_confirm"],
});

type FormData = z.infer<typeof schema>;

export default function ContactForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
    reset,
  } = useForm<FormData>({ resolver: zodResolver(schema) });

  async function onSubmit(data: FormData) {
    const fd = new FormData();
    fd.append("access_key", "YOUR_ACCESS_KEY");
    Object.entries(data).forEach(([k, v]) => fd.append(k, String(v)));

    const res = await fetch("https://splitforms.com/api/submit", {
      method: "POST",
      body: fd,
    });
    if ((await res.json()).success) reset();
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("name")} placeholder="Name" />
      {errors.name && <p>{errors.name.message}</p>}

      <input {...register("email")} type="email" placeholder="Email" />
      {errors.email && <p>{errors.email.message}</p>}

      <input {...register("password")} type="password" placeholder="Password" />
      {errors.password && <p>{errors.password.message}</p>}

      <input {...register("password_confirm")} type="password" placeholder="Confirm password" />
      {errors.password_confirm && <p>{errors.password_confirm.message}</p>}

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? "Sending…" : "Send"}
      </button>
    </form>
  );
}

The modern React form validation stack is React Hook Form (state + form mechanics) plus Zod (typed schemas + validation rules). The combination gives you type-safe form data, inline error rendering, async submission, and minimal re-renders — all in ~20 lines per form.

Define your validation rules as a Zod schema. Each field gets a type and constraints — `z.string().min(2)`, `z.string().email()`, `z.number().int().positive()`. The `.refine()` method handles cross-field rules like password confirmation. The schema is the single source of truth for both validation logic and TypeScript types via `z.infer<typeof schema>`.

React Hook Form's `useForm` hook gives you `register` (wires inputs into the form state), `handleSubmit` (runs validation, calls your submit function), and `formState.errors` (the validation errors object). Pass `resolver: zodResolver(schema)` to wire Zod in. That's the whole integration.

Submit handler is a regular async function that receives the validated, typed `data` object. Make a fetch call to splitforms's `/api/submit` endpoint, await the response, update UI accordingly. The `isSubmitting` boolean from formState handles the disable-during-submit pattern automatically.

How to set this up

Step 01

Install dependencies

npm install react-hook-form @hookform/resolvers zod — three packages, ~25KB minified combined.

Step 02

Define a Zod schema

One schema per form. Each field has a type + constraints. .refine() for cross-field rules. .infer<typeof schema> gives you the TypeScript type.

Step 03

Wire useForm with zodResolver

useForm({ resolver: zodResolver(schema) }) connects RHF state management to your Zod validation.

Step 04

Render and submit

register('field') on each input, errors.field?.message for inline error display, handleSubmit(onSubmit) wires the submit.

20 lines per form, fully typed, ~25KB total runtime.

Frequently asked questions

What's the best React form validation library?

React Hook Form + Zod is the 2026 default for new React projects. RHF handles state and re-renders efficiently; Zod handles validation rules and type generation. Together they're typed, performant, and ~25KB combined.

How do I validate a form in React?

Use useForm from react-hook-form with a zodResolver. Define a Zod schema for your fields. Call register(fieldName) on each input. handleSubmit(onSubmit) runs validation; errors render from formState.errors.

How do I do password confirmation in React Hook Form?

Use Zod's .refine() method on the schema: `.refine((d) => d.password === d.password_confirm, { message: "Passwords don't match", path: ["password_confirm"] })`. The error attaches to the password_confirm field.

Yup vs Zod — which should I use?

Zod for new projects. It's TypeScript-first (Yup was retrofitted), produces better type inference, and has a more modern API. Yup is still fine if you're maintaining an existing codebase — both pair with React Hook Form via the @hookform/resolvers package.

How do I handle async validation (e.g., 'is email taken')?

Zod doesn't do async validation natively. Use React Hook Form's `mode: 'onBlur'` and add async validation in onSubmit, or use the `setError` API to add a server-returned validation error after the submission fails.

Does React Hook Form work with server components?

React Hook Form runs in client components only ('use client' directive required). For server-component-only validation, use Next.js Server Actions with Zod schemas — same Zod schema, different invocation path.

Related guides

Form validation

jQuery Form Validation — Working Code for 2026

Form validation

Bootstrap Form Validation — Native HTML + Bootstrap 5

HTML forms

HTML Form — How to Build and Submit Forms in HTML

Ship the form, not the backend.

Free for 1,000 submissions/month. Email delivery, AI spam filtering, signed webhooks, real dashboard — all on the free plan. No credit card.

Get a free access key →