# 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 for form types  (path: /src/lib/schemas)
<!-- scope: folder | priority: high | strict -->

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

- Infer types with `z.infer<typeof schema>`; never declare a parallel `interface` or `type` for the same shape.
- When the schema uses `.transform()`, `.default()`, `.pipe()`, or `z.coerce.*`, the input and output types diverge. Keep both available: `z.input<typeof schema>` is what the form fields hold, `z.output<typeof schema>` is what the submit handler receives after validation. Do not collapse them into one type.
- Export the schema from `/src/lib/schemas` so client forms and server handlers import the same object, not two copies.
- Put messages in the schema (`z.string().min(1, 'Required')`) so client and server emit identical errors.
- Cross-field rules belong in the schema via `.refine()` or `.superRefine()` (`.superRefine` is supported again as of Zod 4.x and was un-deprecated in 2025). Note `ctx.path` was removed in Zod 4: pass an explicit `path` array to `ctx.addIssue({ code: 'custom', path: ['confirmPassword'], message: '...' })`.

---

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

Every `useForm` call is resolver-backed, correctly typed, and fully initialized. This assumes `@hookform/resolvers` v5 (requires `react-hook-form` >= 7.55).

- Pass `resolver: zodResolver(schema)` from `@hookform/resolvers/zod`. One resolver handles Zod 3, Zod 4, and Zod 4 mini, so no version branching is needed.
- When the schema has transforms/coercion/defaults, use the three-generic signature so the submit handler sees the parsed output type: `useForm<z.input<typeof schema>, unknown, z.output<typeof schema>>({ resolver: zodResolver(schema), defaultValues })`. Omitting the third generic is the most common v5 type error: `handleSubmit` data is then typed as the input, not the transformed output.
- Provide a complete `defaultValues` object covering every field so inputs are controlled from first render and React never warns about a controlled/uncontrolled flip.
- Default (submit-time) `mode` is correct for most forms; only opt into `mode: 'onChange'` or `'onBlur'` when the UX needs it, since per-keystroke validation costs re-renders.
- Keep inputs uncontrolled via `register` or `Controller`; do not mirror field values into local `useState`.
- If you standardize on Standard Schema across libraries, `standardSchemaResolver` is the alternative, but prefer `zodResolver` for Zod-only projects.

---

### [RULE] Server must re-parse with the same schema before any write  (path: /app)
<!-- scope: folder | priority: high | strict -->

Client-side validation is a UX affordance, not a security boundary. The server re-validates on every mutation.

- In a Server Action or API route, call `schema.safeParse(input)` using the exact schema the form imports. Never skip this because React Hook Form already validated, and never trust the raw payload.
- On failure, build field errors with `z.flattenError(result.error)` (Zod 4). The instance method `result.error.flatten()` is deprecated in Zod 4. `z.flattenError` returns `{ formErrors, fieldErrors }`; use `z.treeifyError` for nested schemas.
- Coerce and sanitize on the server through the schema (`z.coerce.number()`, `.trim()`) rather than re-implementing checks by hand, so server and client stay in lockstep.
- Return field errors to the client so they can be mapped back inline with `setError`, instead of swallowing them into a generic toast.

---

### [MEMORY] Submission, server errors, and reset are driven from formState  (path: /src/components/forms)

Submission UX reads from `formState`, never from a parallel loading flag we maintain ourselves.

- Gate the submit button on `formState.isSubmitting`, and surface readiness with `isValid` / `isDirty`, rather than tracking booleans manually.
- Pair the form with `useActionState` (React 19) so pending state and the action's returned errors thread through without extra client state. The Server Action returns the `z.flattenError` field errors and we map them on the client.
- Map server-side failures back onto fields with `setError`, and use `setError('root.serverError', { message })` for non-field errors so they render inline, not only as a toast.
- Throwing inside the async `handleSubmit` callback is acceptable; catch it and convert to a `setError` call.
- After a successful save, call `reset(serverConfirmedValues)` so the dirty baseline matches what was actually persisted (avoids a form that still looks dirty post-save).
- Read live values with `watch` only where a render genuinely depends on them; prefer `getValues` for one-off reads inside handlers to avoid re-render churn.

---

### [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, targeting @hookform/resolvers v5 and Zod 4. Run before merging any new or changed form to confirm schema-first typing, correct useForm generics, a complete defaultValues object, and server-side re-validation parity.
---

# 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` (v5, with `react-hook-form` >= 7.55).
- [ ] When the schema has transforms/coercion/defaults, `useForm` uses the three-generic form `useForm<z.input<...>, unknown, z.output<...>>` so the submit handler sees the parsed output type.
- [ ] `defaultValues` covers every field so no input flips between controlled and uncontrolled.
- [ ] Validation `mode` matches the intended UX; `onChange`/`onBlur` is a deliberate choice, not 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 built with `z.flattenError(error)` (not the deprecated `error.flatten()`), returned, and mapped back with `setError`; errors show inline, not only as a toast.
- [ ] Submit UX reads `formState.isSubmitting` / `isValid`; pending state uses `useActionState` where applicable, not a hand-rolled loading flag.
- [ ] Successful submit calls `reset(serverConfirmedValues)` to refresh the dirty baseline.
- [ ] Cross-field rules live in `.refine()` / `.superRefine()` with an explicit `path` (Zod 4 removed `ctx.path`), not in component effects.
