Files
TREK/client/src/pages/PATTERN.md
T
Maurice fc7d8b5d12 Migrate TREK 3 to NestJS + React 19 with a shared Zod contract layer
Brownfield strangler migration of the backend onto NestJS modules
(auth, trips, days, places, assignments, packing, todo, budget,
reservations, collab, files, photos, journey, share, settings, backup,
oidc, oauth, admin, atlas, vacay, weather, airports, maps, categories,
tags, notifications, system-notices) served through a per-prefix
dispatcher, keeping the existing SQLite/better-sqlite3 DB and JWT
httpOnly cookie auth, with behavioural parity for every route.

Client: React 19 upgrade, "page = wiring container + data hook"
pattern across all pages, per-domain Zustand stores bound to
@trek/shared contracts, and decomposition of the large components
(DayPlanSidebar, PackingListPanel, CollabNotes, FileManager,
MemoriesPanel, PlacesSidebar, CollabChat, SystemNoticeModal,
BudgetPanel, PlaceFormModal, ...) into focused render units backed by
in-file hooks.

Apply the shared global request pipeline (helmet/CSP, CORS, HSTS,
forced HTTPS, the global MFA policy and request logging) to the NestJS
instance as well, so a migrated route is protected identically to the
legacy fallback rather than bypassing it.
2026-05-30 02:39:26 +02:00

2.5 KiB

Page pattern: wiring container + data hook

Every page under src/pages follows the same shape: the exported *Page component is a thin wiring container and all of its state, effects, data loading and event handlers live in a co-located use<Page>() hook.

src/pages/
  DashboardPage.tsx          ← container: reads the hook, renders JSX
  dashboard/
    useDashboard.ts          ← state, effects, API calls, handlers
    dashboardModel.ts        ← (optional) pure types + helpers, no React

What goes where

The hook (use<Page>.ts) owns everything stateful:

  • useState / useReducer / useRef
  • useEffect / useLayoutEffect
  • useMemo / useCallback
  • store selectors, API calls, WebSocket listeners, handlers
  • derived values

It returns a single object the page destructures.

The page (*Page.tsx) is presentation only:

  • const { ... } = use<Page>()
  • useTranslation() for t/locale (a context hook, not state — allowed)
  • JSX, and t-dependent display arrays like the tab list
  • presentational sub-components and pure helpers may live in the same file, before or after the default export
export default function DashboardPage() {
  const { t } = useTranslation()
  const { trips, isLoading, handleCreate } = useDashboard()
  if (isLoading) return <Spinner />
  return <Grid trips={trips} onCreate={handleCreate} />
}

Why

  • Testable — page tests render JSX; hook logic is isolated and mockable.
  • Readable — the container reads top-to-bottom as "what the page shows".
  • Diffable — logic changes touch the hook, layout changes touch the page.

Notes

  • A <page>Model.ts is optional — use it for pure types and helpers shared between the hook and the page (no React imports). See atlas/atlasModel.ts for a mutable-lookup-table example and admin/adminModel.ts for types only.
  • The post-guard derivations that depend on a now-narrowed value (e.g. after if (!current) return) may stay in the page next to the JSX that uses them.
  • Keep the rendered JSX byte-identical when extracting — this is a refactor of where logic lives, not a redesign.

Enforcement

npm run lint:pages (scripts/check-page-pattern.mjs) scans each *Page.tsx default-export body and fails if it calls useState, useReducer, useEffect, useLayoutEffect, useMemo, useCallback or useRef directly. Move that logic into the page's hook. Sub-components and helper hooks in the same file are not flagged.