shadcn-data-table-rhf
shadcn-data-table-rhf documents how to build editable, large-row-count data tables on top of shadcn's Table/useReactTable primitives with react-hook-form as the single source of truth, using @cogs/react-hook-form (useControlledFields, updateRows, lookupLocationTarget) and @cogs/react-table (useRefReactTable, useTableState, diffRowIds). The patterns aren't generic react-hook-form theorizing — they're mined from a production deployment-manifest editor that supports bulk add/remove of rows, duplicate rows that must stay in sync, and per-row zod validation at a scale where a naive per-cell approach visibly drops frames.
What it does
The skill walks through the layer that sits above a single-object form once "form" becomes "form with an editable array of rows": row identity via useFieldArray, merging that metadata with live edited values without flicker, keeping per-cell reads/writes cheap at row-count × column-count scale, fanning one edit out to every occurrence of a duplicated row, bulk-updating a field across a set of selected rows, and validating rows against the same zod schema the form's resolver already uses. It also documents the official useFieldArray gotchas that specifically bite inside a table (index-as-key, object-only entries, no shouldUnregister: true, no back-to-back field-array mutations in one handler), and draws an explicit line to a companion skill, nuqs-table-url-state, for the pagination/filter/sort layer it deliberately doesn't re-derive.
Why it was created
The clearest evidence is the skill's own opening line: it names itself "the cogs canonical version" of this pattern and states it "supersedes the old tanstack-table-patterns skill's meta.updateData/inline-validate approach (raw uncontrolled inputs, no RHF integration, no shared dirty/error state)." SKILL.md's provenance note is equally direct — the patterns are "mined from envmgr-ui's actual production deployment-manifest editor (CreateDeploymentForm.tsx + FormHierarchyPageWithDeploymentManifestServiceColumns.tsx + FormHierarchyEditableCells.tsx), a table that supports bulk add/remove of rows, duplicate ('mirrored') rows that must stay in sync, and per-row zod validation, at a row count where a Controller-per-cell approach visibly drops frames."
Git history corroborates this as a migration, not a from-scratch invention: commit c91a079 ("feat(skills): add shadcn-react-hook-form-forms and shadcn-data-table-rhf, ported from envmgr-ui's RHF/table patterns") describes "the envmgr-ui-validated editable-row pattern (useFieldArray + useControlledFields, register-for-ref-only, mirror-map writes) replacing the outdated meta.updateData/validate cell approach." A follow-up commit, dfff269 ("feat(skills): add metadata.json for 4 skills missing it, all fully validated"), added the skill's metadata.json and folded it into the npm-publishable skill set alongside four siblings.
No docs/sessions/ or docs/plans/ writeup narrates why this skill was built, so beyond this abstract and the two commits above, there's no separate founding dossier — documented provenance comes from the skill's own metadata and commit messages, not an incident writeup.
How it works
The skill is organized as a numbered walkthrough, and each step exists to close a specific failure mode rather than as a stylistic preference:
useFieldArraywithout avalues:prop. Passing a live object touseForm({ values })triggers an implicitreset()on every reference change — in an editable table that's mutated continuously (add row, remove row, background patch), that would clobber whatever the user is mid-typing elsewhere. The skill uses explicitreset()/setValue()instead, pairing intentional resets withkeepDirty/keepDirtyValuesso in-flight edits survive a background refresh.useWatch+useControlledFieldsto merge row metadata with live values.useFieldArray'sfieldsarray is mostly identity metadata; a hand-rolled merge withuseWatchcan render an empty array for one tick during a reset and flash the table to "no rows."useControlledFields(fields, watchedRows, allowEmpty)retains the last non-empty merged array across that tick unlessallowEmpty: trueis passed for a screen where an empty table is a legitimate state.register()for the ref only,getValue()/setValue()for reads and writes, instead of aController/FormFieldper cell. For small tables, aControllerper cell (the sibling skill's approach) is fine. At row × column scale it means every keystroke can ripple into recomputing the whole array-bound region — so cells stay mostly-uncontrolled inputs that RHF tracks by ref, with reads from TanStack Table's owngetValue()and writes going straight throughsetValue().- The mirror-map pattern (
buildIdToPathMirrorMap/updateMirroredFields) for rows that appear more than once (a parent/child hierarchy, a service that's both a standalone row and nested under a group) — fanning one edit to every occurrence via repeatedsetValue()calls rather than an array-wide replace. The skill is explicit that@cogs/react-hook-formships the underlying primitive (lookupLocationTarget) but not yet these two helpers themselves, which currently live only in envmgr-ui's own packages — porting them locally is called out as a prerequisite, not an assumption. updateRowsfor the "apply this field to N selected rows" shape, returning new object references only for touched rows so RHF/React's reference-based dirty-tracking and memoization aren't defeated by an unconditionalrows.map(r => ({...r, ...patch})).- Schema-driven row validation instead of per-field
fieldState. Because cells are read viagetValue()/setValue()rather thanController, they have nofieldState.errorto render — the skill validates a row's data withschema.safeParse()against the same zod schema the form's resolver already uses, so "what the form considers valid" and "what a cell renders as an error" can't drift apart.
A dedicated "Red Flags" section pre-empts a specific wrong turn: a newer shadcn form convention (Field/FieldGroup/standardSchemaResolver) exists elsewhere, but this codebase deliberately keeps the classic FormField/FormControl/FormMessage + zodResolver convention because it's what's already proven in the source codebase this skill was validated against — the skill tells readers not to "fix" that as a modernization.
When to use it / when not to
Use this skill when a table's rows are edited in place and those edits need to compose with a form's isDirty/isValid/submit flow — rows can be added, removed, or duplicated with edits that must propagate to mirrors, the table is large enough that a Controller/FormField per cell causes visible re-render cost, or row validation needs to share a zod schema with the rest of the form. It's also the porting target for any table still built on the superseded tanstack-table-patterns style (per-cell meta.updateData plus inline validate, no RHF integration).
Skip it for a read-only or simple sortable table with no form underneath — shadcn's plain DataTable (useReactTable + Table/TableBody/TableCell/TableRow) is sufficient there. It's also not the URL-state/pagination/filter layer: that's the sibling skill nuqs-table-url-state, which this skill cross-references rather than re-derives. For a single-object form with no row array at all, the sibling skill shadcn-react-hook-form-forms covers the basic FormField/Controller pattern this skill deliberately skips once a table needs to scale.