# Pathrule Pattern: Forms with React Hook Form + Zod (1.0.0)
# ::pathrule:package:forms-rhf-zod

### [RULE] Zod schema is the single source of truth  (path: /src/lib/schemas)
<!-- scope: folder | priority: high | strict -->

Each form has exactly one Zod schema, and every type comes from it.

- Infer types with `z.infer<typeof schema>`; never declare a parallel `interface` or `type` for the same shape.
- 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.
- Export the schema from `/src/lib/schemas` so client forms and server handlers import the same object.
- Keep messages in the schema (`z.string().min(1, 'Required')`) so client and server emit identical errors.

---

### [RULE] Wire useForm with a resolver and full defaultValues  (path: /src/components/forms)
<!-- scope: folder | priority: high | advisory -->

Every `useForm` call is resolver-backed and fully initialized.

- Pass `resolver: zodResolver(schema)` from `@hookform/resolvers/zod`; it auto-detects Zod 3 vs 4, so no version branching.
- Provide a complete `defaultValues` object covering every field so inputs stay controlled-from-the-start and React never warns about controlled/uncontrolled flips.
- 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.
- Keep inputs uncontrolled via `register` or `Controller`; do not mirror field values into local `useState`.

---

### [MEMORY] Re-parse with the same schema in Server Actions  (path: /app)

Client validation is a UX affordance, not a security boundary, so the server re-parses.

- 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.
- 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.
- Combine with `useActionState` (React 19) to thread pending state and returned errors through the action without extra client state.
- Coerce and sanitize on the server side via the schema (`z.coerce.number()`, `.trim()`) rather than re-implementing checks by hand.

---

### [MEMORY] Async submit, errors, and reset patterns  (path: /src/components/forms)

Submission UX reads from `formState`, never from a separate loading flag.

- Gate the submit button on `formState.isSubmitting` and surface validity with `isValid` / `isDirty` instead of tracking booleans manually.
- Throwing inside the `handleSubmit` async callback is fine; catch it to map a server error with `setError('root.serverError', ...)`.
- After a successful save, call `reset(serverConfirmedValues)` so the form's dirty baseline matches what was persisted.
- 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.

---

### [SKILL] forms-rhf-zod-review  (path: /)

---
name: forms-rhf-zod-review
description: 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.
---

# Forms with React Hook Form + Zod review

- [ ] 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.
- [ ] `useForm` uses `resolver: zodResolver(schema)` from `@hookform/resolvers/zod`.
- [ ] `defaultValues` covers every field so no input flips between controlled and uncontrolled.
- [ ] Validation `mode` matches the intended UX; `onChange` is justified, not the default copy-paste.
- [ ] Inputs use `register`/`Controller`; field values are not duplicated into local `useState`.
- [ ] The server (Server Action or API route) re-parses with the same schema via `safeParse` before any write.
- [ ] Server field errors are mapped back with `setError`; the form shows inline errors, not only a toast.
- [ ] Submit UX reads `formState.isSubmitting` / `isValid` rather than a separate loading flag.
- [ ] Successful submit calls `reset(serverConfirmedValues)` to refresh the dirty baseline.
- [ ] Cross-field rules live in `.refine()`/`.superRefine()`, not in component effects.
