File size: 2,512 Bytes
cd99321
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# 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.