ProjectR Docs
Dashboard

Patterns

State layering, tables, forms, and performance rules.

State layering (in order of preference)

  1. URL state — nuqs. use-data-table.ts owns page/perPage/sort/per-column filters as query params (Zod-validated parsers in src/lib/parsers.ts, column-id allowlist, debounced 300 ms filter writes that reset page to 1). Tables are manualPagination/Sorting/Filtering: true — server-driven via Convex.
  2. Server state — Convex. Through the hook barrel only.
  3. Forms — React Hook Form + Zod v4. Schemas live in the feature's utils/, bound through shadcn form.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 toast from sonner directly. Destructive actions pair confirmation-dialog.tsx with use-confirmation-dialog.ts.
  • empty-state.tsx for every zero-row surface (icon presets + optional actions).
  • Images: src/components/media/r2-image.tsx detects 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-field in src/components/media/, built on the react-dropzone file-uploader.tsx and the upload hooks in use-r2-upload.ts.

Performance rules

  • Always pass 'skip' for conditional queries.
  • Real useDebounce for search (see Data Layer).
  • Memoize callbacks passed down; optimizePackageImports covers recharts/lucide/table — keep big imports on those paths.
  • Don't fetch in effects what a useQuery subscription can own.

Tooling

  • type-check (tsc) + lint:strict (eslint --max-warnings=0) + format:check (Prettier + tailwindcss plugin) — check-all runs 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 with SENTRY_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 pass dataCollection from src/lib/sentry-data-collection.ts, which sends none of them.

On this page