Patterns
State layering, tables, forms, and performance rules.
State layering (in order of preference)
- URL state — nuqs.
use-data-table.tsownspage/perPage/sort/per-column filters as query params (Zod-validated parsers insrc/lib/parsers.ts, column-id allowlist, debounced 300 ms filter writes that reset page to 1). Tables aremanualPagination/Sorting/Filtering: true— server-driven via Convex. - Server state — Convex. Through the hook barrel only.
- Forms — React Hook Form + Zod v4. Schemas live in the feature's
utils/, bound through shadcnform.tsx.
The dashboard has no global client store. Zustand is not a dashboard dependency, so state that outlives a component lives in the URL or in Convex.
Tables
use-data-table.ts + components/ui/table/* + @tanstack/react-table. Row selection, column visibility, faceted filters, and bulk-action toolbars are per-feature components. Pagination displays "x–y of total" with cursor or offset depending on the backend query.
TanStack Table 9 needs every feature declared up front. src/lib/data-table.ts holds the one feature set the tables share (dataTableFeatures) and the sort and filter functions a column may name, because v9 resolves a string sortFn or filterFn, 'auto' included, only against registered ones. Column types take it first: ColumnDef<DataTableFeatures, Row>. useTable returns a new table object on every render, so an effect depends on a method such as table.setColumnVisibility, never on table; depending on table looped the users page.
UI conventions
- Toasts call
toastfromsonnerdirectly. Destructive actions pairconfirmation-dialog.tsxwithuse-confirmation-dialog.ts. empty-state.tsxfor every zero-row surface (icon presets + optional actions).- Images:
src/components/media/r2-image.tsxdetects R2 keys and resolves signed URLs with skeleton fallback — use it for anything that might be an R2 key. - Uploads:
image-upload-field/video-upload-fieldinsrc/components/media/, built on the react-dropzonefile-uploader.tsxand the upload hooks inuse-r2-upload.ts.
Performance rules
- Always pass
'skip'for conditional queries. - Real
useDebouncefor search (see Data Layer). - Memoize callbacks passed down;
optimizePackageImportscovers recharts/lucide/table — keep big imports on those paths. - Don't fetch in effects what a
useQuerysubscription can own.
Tooling
type-check(tsc) +lint:strict(eslint--max-warnings=0) +format:check(Prettier + tailwindcss plugin) —check-allruns all three.- Vitest + Testing Library (jsdom); tests colocated; CSS imports mocked.
- Sentry: client init is production-only (dev-disabled due to Next 16 listener warnings), tunneled through
/monitoring; Vercel Production and Preview builds create the release, associate commits and upload source maps withSENTRY_AUTH_TOKEN; local and CI builds write nothing to Sentry. Sentry 11 collects user info, cookies and request bodies unless told otherwise, so all three inits passdataCollectionfromsrc/lib/sentry-data-collection.ts, which sends none of them.