ProjectR Docs
Dashboard

Data Layer

Convex hook wrappers, response flattening, and reactivity patterns.

The barrel

src/hooks/convex/index.ts re-exports per-domain files: use-dashboard, use-loyalty (the big one), use-users, use-orders, use-products, use-content, use-notifications, use-ratings, use-duit-raya, use-webhooks, use-webhook-events (delivery pipeline rows, not subscriptions), use-beauty, use-support-messages, use-audit-log, and the R2 trio (use-r2-url, use-r2-files, use-r2-upload). types.ts exports Doc<'table'> / Id<'table'> aliases and Enriched* response types (the backend transforms _id → id).

Hook shape: thin adapters returning { data, isLoading: response === undefined }; mutations are often returned directly.

Response flattening

The backend returns mixed shapes — hooks defensively handle all of them:

  • Wrapped envelope { success, data, message } → unwrap (see useUserDetail)
  • Direct document objects → use as-is
  • Direct payloads with no envelope (e.g. dashboardGetUserStats returns stats directly)

use-loyalty.ts also carries field normalizers mapping backend field names to frontend form names. When the backend response shape of an endpoint changes, fix it in the hook, not the component.

Reactivity rules

  • Server state is Convex subscriptions — never manually refetch.
  • Conditional queries pass 'skip': useQuery(api.x.y, ready ? args : 'skip').
  • Search inputs must use useDebounce (~300 ms). useDeferredValue is banned for Convex args — it doesn't prevent subscription churn.
  • Memoize callbacks passed to children; use skip-initial-mount refs for side effects.
  • use-auth.ts additionally guards the transient null: a profile that regresses to null mid-session is treated as loading (5 s timeout), because Better Auth session updates can briefly get ahead of the Convex auth token.

ID and casing conventions

  • _id = Convex doc ID (aliased id); userId = Better Auth ID. User pages navigate by userId.
  • Statuses are lowercase on the wire — hooks .toLowerCase() before mutation calls. Role casing is lowercase everywhere (normalizeRole() for unknown input).

On this page