Forms with React Hook Form + Zod

Pathrule2 Rules • 2 Memories • 1 Skill

A pattern for building forms with React Hook Form and a Zod resolver where one schema is the single source of truth for types, client validation, and server validation. It keeps inputs uncontrolled for performance, infers TypeScript types directly from the schema, and reuses the same schema in Server Actions or API routes so client and server never drift. Built for Zod 4 and React Hook Form 7.

Suggested path map

Pathrule places each piece on the matching path, so your assistant only sees it where it belongs. This is the scoping you get on import; you can adjust it in your workspace.

/ workspace root
forms-rhf-zod-review
src/
lib/
schemas/
Zod schema is the single source of truth
components/
forms/
Wire useForm with a resolver and full defaultValues
Async submit, errors, and reset patterns
app/
Re-parse with the same schema in Server Actions

Rules

2
Zod schema is the single source of truth/src/lib/schemashighstrictDerive form types from the schema; never hand-write a parallel interface.
1Each form has exactly one Zod schema, and every type comes from it.
2 
3- Infer types with `z.infer<typeof schema>`; never declare a parallel `interface` or `type` for the same shape.
4- When the schema has `.transform()`, `.default()`, or coercion, type `useForm` values with `z.input<typeof schema>` and the parsed result with `z.output<typeof schema>` because they diverge.
5- Export the schema from `/src/lib/schemas` so client forms and server handlers import the same object.
6- Keep messages in the schema (`z.string().min(1, 'Required')`) so client and server emit identical errors.
Wire useForm with a resolver and full defaultValues/src/components/formshighadvisoryEvery form passes the Zod resolver plus a complete defaultValues object.
1Every `useForm` call is resolver-backed and fully initialized.
2 
3- Pass `resolver: zodResolver(schema)` from `@hookform/resolvers/zod`; it auto-detects Zod 3 vs 4, so no version branching.
4- Provide a complete `defaultValues` object covering every field so inputs stay controlled-from-the-start and React never warns about controlled/uncontrolled flips.
5- Default `mode` is fine for submit-time validation; only opt into `mode: 'onChange'` or `onBlur` when the UX needs it, since it costs re-renders.
6- Keep inputs uncontrolled via `register` or `Controller`; do not mirror field values into local `useState`.

Memories

2
Re-parse with the same schema in Server Actions/appServer handlers re-validate with the shared schema before any write.
1Client validation is a UX affordance, not a security boundary, so the server re-parses.
2 
3- In a Server Action or API route, call `schema.safeParse(input)` using the exact schema the form imports; never trust the payload because RHF already validated it.
4- Return `result.error.flatten()` field errors and map them back onto the form with `setError` so server failures surface inline, not just as a toast.
5- Combine with `useActionState` (React 19) to thread pending state and returned errors through the action without extra client state.
6- Coerce and sanitize on the server side via the schema (`z.coerce.number()`, `.trim()`) rather than re-implementing checks by hand.
Async submit, errors, and reset patterns/src/components/formsDrive submission UX from formState and reset with server-confirmed values.
1Submission UX reads from `formState`, never from a separate loading flag.
2 
3- Gate the submit button on `formState.isSubmitting` and surface validity with `isValid` / `isDirty` instead of tracking booleans manually.
4- Throwing inside the `handleSubmit` async callback is fine; catch it to map a server error with `setError('root.serverError', ...)`.
5- After a successful save, call `reset(serverConfirmedValues)` so the form's dirty baseline matches what was persisted.
6- For dependent or cross-field rules use `.refine()` / `.superRefine()` in the schema, and read live values with `watch` only where a render genuinely depends on them.

Skills

1
forms-rhf-zod-review/rootPre-merge checklist for React Hook Form + Zod forms.
1---
2name: forms-rhf-zod-review
3description: Review checklist for forms built with React Hook Form and a Zod resolver. Run before merging any new or changed form to confirm schema-first typing, a complete defaultValues object, and server-side re-validation parity.
4---
5 
6# Forms with React Hook Form + Zod review
7 
8- [ ] One Zod schema is the source of truth; types come from `z.infer` (or `z.input`/`z.output` when transforms diverge), not a hand-written interface.
9- [ ] `useForm` uses `resolver: zodResolver(schema)` from `@hookform/resolvers/zod`.
10- [ ] `defaultValues` covers every field so no input flips between controlled and uncontrolled.
11- [ ] Validation `mode` matches the intended UX; `onChange` is justified, not the default copy-paste.
12- [ ] Inputs use `register`/`Controller`; field values are not duplicated into local `useState`.
13- [ ] The server (Server Action or API route) re-parses with the same schema via `safeParse` before any write.
14- [ ] Server field errors are mapped back with `setError`; the form shows inline errors, not only a toast.
15- [ ] Submit UX reads `formState.isSubmitting` / `isValid` rather than a separate loading flag.
16- [ ] Successful submit calls `reset(serverConfirmedValues)` to refresh the dirty baseline.
17- [ ] Cross-field rules live in `.refine()`/`.superRefine()`, not in component effects.

Why this pattern

Form types, client validation, and server validation drift apart and re-render the whole form on every keystroke.

Built for Frontend and full-stack teams building React or Next.js forms with TypeScript.

Keeps your assistant from:

  • Hand-writing TypeScript types that drift from the Zod schema instead of inferring them
  • Trusting client-side validation only and skipping a server-side re-parse
  • Controlling every input with local useState and re-rendering the whole form on each keystroke
License
Apache-2.0
Version
1.0.0
Updated
2026-06-09
View source