---
name: build-react-form-with-validation
description: Builds an accessible React form with React Hook Form and Zod schema validation, inline error messages and a loading submit state. Use when the user wants a signup, contact, checkout or settings form, form validation, required fields, or error messages under inputs.
license: CC-BY-4.0
metadata:
  category: frontend
  tags: "react,forms,zod,typescript"
  author: johnny-bravo
  version: "1.0"
  updated: "2026-10-30"
  status: published
---

# Build a React form with validation

## When to use
- Any form with more than two fields, or any form that posts to an API.
- Replacing ad-hoc `useState` per field with one schema that is the single source of truth.

## Steps
1. Install: `npm i react-hook-form zod @hookform/resolvers`.
2. Define the schema once and infer the type from it:
   ```ts
   const Schema = z.object({
     email: z.string().email("Enter a valid email"),
     password: z.string().min(8, "At least 8 characters"),
   });
   type FormValues = z.infer<typeof Schema>;
   ```
3. Wire the form: `useForm<FormValues>({ resolver: zodResolver(Schema), mode: "onBlur" })`.
4. For each field render a `<label htmlFor>`, the input with `{...register("email")}`, `aria-invalid={!!errors.email}` and `aria-describedby="email-error"`, and the error in `<p id="email-error" role="alert">`.
5. In `onSubmit`, disable the button while `formState.isSubmitting`, call the API, and map server errors back with `setError("email", { message })`.
6. Re-use the same Zod schema on the server to validate the request body.

## Gotchas
- `mode: "onChange"` on every field feels nagging; validate on blur, re-validate on change after the first error.
- Number inputs return strings: use `z.coerce.number()`.
- Never rely on client validation alone; always validate again on the server.

## Example prompt
"Add a signup form with email and password that shows errors under each field."
