Files
2026-07-12 10:17:17 -04:00

55 lines
3.5 KiB
Markdown

# React Component Extraction — Mega-File Split Pattern
## When you need this
A single `.tsx` file has grown past ~1,000 lines with inline sub-components, inline helper functions, and inline type definitions. The established pattern is:
1. **Pure helpers → `src/lib/<domain>.ts`** — formatters, PB query builders, status label/color mappers, date formatters
2. **Types → shared module** — interfaces can stay in the page file if re-exported (for external importers), or move to `src/components/<domain>/types.ts`
3. **Inline components → `src/components/<domain>/`** — one file per component, barrel `index.ts`
4. **Page becomes orchestrator** — imports from barrel, keeps all state + handlers
## Proven pattern (from Appointments.tsx, Customers.tsx splits)
| Extraction target | Destination | Pattern |
|---|---|---|
| `function HelperFn(...)` | `src/lib/<domain>.ts` | Pure function, no hooks, no JSX |
| `interface X` | `src/components/<domain>/types.ts` (or re-export from page) | Re-export from page if external files import from the old path |
| `function Skeleton()`, `function EmptyState()` | `EmptyStates.tsx` | Plain function component |
| `function Card(props): JSX` | `<Name>Card.tsx` | `memo(function NameCardImpl(props) { ... })` |
| `function FormModal(props)` | `<Name>FormModal.tsx` | Keep as-is (has internal state — memo adds nothing) |
| `function DetailView(props)` | `<Name>DetailView.tsx` | `memo(function DetailViewImpl(props) { ... })` |
| `function DeleteConfirmModal(props)` | `DeleteConfirmModal.tsx` | Keep as-is |
| Barrel | `index.ts` | Re-export all components and types |
## Import compatibility pattern
When external files import types from the old page path (e.g., `import type { CustomerWithVehicles } from '../pages/Customers'`), the page MUST re-export those types:
```tsx
// src/pages/Customers.tsx — at the top level
export type { CustomerRecord, CustomerWithVehicles, CustomerFormData }
from '../components/customers/types';
```
Do NOT change the import paths in the 5 external files — the re-export keeps them working.
## Pitfalls discovered during extraction
### 1. Fragment shorthand → Fragment key mismatch
The original file uses `<>...</>` (no key prop). The extracted equivalent needs `<Fragment key={expr}>`. But the closing paren/bracket structure differs because the Fragment opening tag is part of the JSX tree rather than being a structural wrapper.
**Trace template:**
```
Original: return ( <><tr/><tr/></> );
After extract: return ( <Fragment key={x}><tr/><tr/></Fragment> );
```
The closing `));` after `</>` in a `.map()` callback becomes `);` after `</Fragment>`, then `})}` for block-end, `.map()` close, JSX expression close. **Count explicitly.**
### 2. `useCallback` dependency drift
When moving a handler from the orchestrator page that references a callback defined in the same component, the `useCallback` deps array may reference a function that's now imported. The import is stable (never changes identity), so the dep IS needed in the array to be correct, but TypeScript won't warn either way. **Check:** every `useCallback(fn, deps)` where `fn` calls an imported function — that imported function should be in the deps array, or the callback is stale.
### 3. `formatDate` name collision
The page likely has its own `function formatDate(...)` helper. The extracted lib module uses the same name. Rename to `formatCustomerDate` or `formatDomainDate` — the app's `src/lib/format.ts` may already export a global `formatDate`.