Spaces:
Sleeping
Sleeping
| # 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 | |
| ```tsx | |
| 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. | |