shadcn-react-hook-form-forms
shadcn-react-hook-form-forms wires shadcn's Form/FormField/FormControl/FormMessage primitives to react-hook-form for the single-object-form case — one object, one useForm, one zodResolver — including plugging a kubb-generated OpenAPI zod schema straight into zodResolver. Its gotchas aren't hypothesized from library docs: they were found by actually compiling the patterns against this repo's real generated types, which is why the skill can be specific about exactly which schema shapes break and why.
What it does
The skill covers the layer directly under most shadcn forms in this codebase:
- The anatomy of shadcn's
Formwrapper —FormisFormProviderre-exported,FormFieldis aControllerplus a context provider for the fieldname,FormControlis a RadixSlot.Rootthat mergesid/aria-*onto its single child rather than rendering its own DOM node, anduseFormField()is the hook that ties context +useFormContext()/useFormState()together for every other primitive to read. - A before/after conversion from a raw
<input>+ manualuseState+ hand-rolled error<p>to the shadcn primitives with validation centralized in a zod schema. - Wiring a kubb-generated zod schema (e.g.
packages/api-client/src/zod/<model>Schema.ts) directly intozodResolver, plus the concrete gotchas that surface once you do. - A decision table for when a single-object form needs
@cogs/react-hook-form's collection-oriented helpers versus when plainreact-hook-formis enough. - A no-new-dependency multi-step form pattern — local
stepstate,form.trigger()per step,handleSubmitgated to the final step. - Surfacing server-side validation errors through
form.setError, including theroot-level case that bypassesuseFormField().
Why it was created
Documented provenance comes from the skill's own metadata rather than a separate session dossier: the only relevant pre-existing hit is docs/sessions/2026-08-02-cogs-package-migration/ (about inlining the @cogs/react-hook-form package's internals during a monorepo migration, not about this skill), so there's no dedicated founding narrative beyond what the skill itself records (this page's own "Why it was created" section is the closest thing).
That metadata is specific about what backs it, though. The abstract in metadata.json states the patterns were "verified end-to-end against this repo's real generated types, not hypothesized from docs" — the kubb-schema wiring example, the nullable-field gotcha, and the version-alignment note were all confirmed by compiling against real repo types rather than written from general zodResolver advice. Git history corroborates the origin: commit c91a079 ("feat(skills): add shadcn-react-hook-form-forms and shadcn-data-table-rhf, ported from envmgr-ui's RHF/table patterns") introduced this skill alongside its sibling, describing it as porting patterns from the envmgr-ui monorepo and validating the zodResolver wiring "against offerly's real generated types." A later commit, dfff269 ("feat(skills): add metadata.json for 4 skills missing it, all fully validated"), added the formal metadata.json abstract used above, for npm packaging — the underlying evidence predates that commit.
How it works
shadcn's Form primitives are a thin wrapper, not a new abstraction. FormField = Controller + a context provider stashing the field name; FormItem mints an id via useId() so FormLabel's htmlFor and FormControl's id agree without an explicit prop; FormControl's Slot.Root is why <FormControl><Input {...field} /></FormControl> works — it merges id/aria-describedby/aria-invalid onto its single child instead of rendering its own element; FormMessage renders error.message or falls back to children, rendering nothing when there's no error, so a bare <FormMessage /> can sit under every field unconditionally.
A kubb-generated zod schema plugs into zodResolver directly — this repo's canonical example feeds a kubb-emitted projectSchema straight into useForm({ resolver: zodResolver(projectSchema) }) with no manual patching. But the skill is equally emphatic about the gotchas that surfaced once real generated types were involved, all "verified" rather than generic:
- Nullable generated fields infer as
T | null, notT | undefined. Spreading{...field}on a nullable field passesvalue={null}into the DOM and triggers React warnings; every nullable field needsvalue={field.value ?? ''}instead. - Catchall/untyped-JSON-blob schemas aren't valid resolver targets — e.g. this repo's
resumeDetailSchema, or thefactLibrary/lastScreenScorefields onresumeSchema. Pick a flat, fully-typed schema (or a.pick()/.omit()slice) instead. - A full generated "record" schema usually needs
.pick()/.omit()of server-owned fields (id,ownerUserId, timestamps) before it's a sane resolver target for a create form. - Version alignment matters:
@hookform/resolvers@^3.9.1+zod@^4.4.3compiled with zero generic mismatches in this repo, even though kubb schemas internally importzod/v4while consumer code imports plainzod— worth re-checking before assuming it holds on a different zod pin.
Plain react-hook-form is the default; @cogs/react-hook-form's helpers (useControlledFields, useRowStatuses, updateRows, and friends) are all collection-oriented and have nothing to attach to on a single-object form — reach for them only when the form is genuinely a field-array/multi-row/dirty-tracking case.
Multi-step forms need no new dependency — a local step index, form.trigger([...fieldNames]) per step to validate just that step, and handleSubmit wired only to the final step's submit button, with a single useForm instance staying the source of truth throughout.
A red flag worth calling out explicitly: the skill tells you not to "upgrade" this convention to the newer shadcn Field/FieldGroup/FieldLabel/FieldError primitives with standardSchemaResolver. That's a genuinely different primitive set that shows up in newer shadcn docs, but this repo's shipped implementation is built on the classic FormField/FormControl/FormMessage + zodResolver convention deliberately — not as something stale waiting to be modernized.
When to use it / when not to
Reach for this skill when converting a raw <input>/<select> + useState form to shadcn's Form primitives, wiring a kubb-generated zod schema into zodResolver, deciding whether @cogs/react-hook-form is warranted, building a lightweight multi-step form, or surfacing server-side errors via form.setError.
It's not the right tool for field arrays, editable/inline-editable tables, multi-row forms, or anything needing per-row dirty/error tracking — that's the sibling skill shadcn-data-table-rhf, which covers @cogs/react-hook-form's row-status and selective-dirty toolkit in depth. The two skills are companions by design: this one is the single-object-form layer, the other is the multi-row/data-table layer built on top of it.