238 lines
9.1 KiB
Markdown
238 lines
9.1 KiB
Markdown
---
|
|
name: react-component-extraction
|
|
description: "Systematically extract inline components, types, and helpers from a monolithic React page component into a clean feature-directory module with barrel exports. Proven on 1000-1900 line TSX files."
|
|
version: 1.0.0
|
|
author: Hermes Agent
|
|
license: MIT
|
|
platforms: [linux, macos]
|
|
metadata:
|
|
hermes:
|
|
tags: [react, refactor, component-extraction, mega-component, barrel, typescript]
|
|
related_skills: [simplify-code, codebase-audit, plan]
|
|
---
|
|
|
|
# React Component Extraction (Mega-Component Splitting)
|
|
|
|
Extract inline components, pure helpers, types, and constants from a monolithic
|
|
React page file into a `src/components/<feature>/` directory with a barrel
|
|
export, reducing the main file from 1000-1900+ lines to under 600 lines.
|
|
|
|
**When to use:** The user says "split this component," "extract the inline
|
|
components," "refactor this page," or you're facing a file over ~800 lines
|
|
with multiple inline component definitions, pure helper functions, and type
|
|
aliases all in one file.
|
|
|
|
**When NOT to use:** Single-file debugging, adding a new feature (use `plan` +
|
|
`subagent-driven-development`), small edits under 50 lines.
|
|
|
|
## Core Pattern
|
|
|
|
A React page file typically contains four categories of code that should live
|
|
separately:
|
|
|
|
| Category | Destination |
|
|
|----------|-------------|
|
|
| Pure helper functions (no hooks, no JSX) | `src/lib/<feature>.ts` |
|
|
| Type aliases, constants, localStorage helpers | `src/components/<feature>/types.ts` |
|
|
| Inline component definitions (JSX + hooks) | `src/components/<feature>/ComponentName.tsx` |
|
|
| Barrel re-exports | `src/components/<feature>/index.ts` |
|
|
|
|
The main file keeps: state hooks, effects, callbacks, CRUD handlers, filter
|
|
logic, the primary render tree, and the default export.
|
|
|
|
## Step-by-Step Process
|
|
|
|
### Phase 1 — Survey the file
|
|
|
|
Read the full file. Use `search_files` to catalog all:
|
|
|
|
- `function ComponentName` / `const ComponentName` — inline components
|
|
- `function helperName` — pure helpers (no hooks, no JSX)
|
|
- `interface` / `type` — type aliases
|
|
- `const` / `let` at module scope — constants
|
|
- `import` lines — categorize which go to which destination
|
|
|
|
Determine the dependency graph: which helpers/imports does each inline
|
|
component need? (Hook calls like `useSettings()` travel with the component.)
|
|
|
|
### Phase 2 — Create `src/lib/<feature>.ts`
|
|
|
|
Move all **pure helper functions** here. These are functions that:
|
|
- Take input, return output (no hooks, no JSX, no React API)
|
|
- Import from: `../../lib/*`, `../../types`, third-party libs
|
|
- Do NOT import from `../../store/*` (Zustand hooks belong in components)
|
|
|
|
```typescript
|
|
// Example: src/lib/repairOrders.ts
|
|
import { isVoided } from './roEvents';
|
|
import { formatDateTime } from './format';
|
|
import type { RepairOrder } from '../types';
|
|
|
|
export function calcDueTime(ro: RepairOrder): number | null { ... }
|
|
export function formatTimeRemaining(ro: RepairOrder, now: number): string { ... }
|
|
export function displayCustomerName(name?: string): string { ... }
|
|
export function normalizeRO(item: any): RepairOrder { ... }
|
|
```
|
|
|
|
Fix relative imports since the file moves from `src/pages/` to `src/lib/`:
|
|
- `../lib/foo` → `./foo`
|
|
- `../types` → `../types`
|
|
|
|
### Phase 3 — Create `src/components/<feature>/types.ts`
|
|
|
|
Move type aliases, interfaces, string-literal unions, and module-level
|
|
constants here. Also include localStorage helpers (load/persist).
|
|
|
|
```typescript
|
|
// Example: src/components/repairOrders/types.ts
|
|
export type TabId = 'active' | 'all' | 'completed';
|
|
export const TABS: { id: TabId; label: string }[] = [ ... ];
|
|
|
|
export interface SavedView { ... }
|
|
export function loadSavedViews(): SavedView[] { ... }
|
|
export function persistSavedViews(views: SavedView[]) { ... }
|
|
```
|
|
|
|
### Phase 4 — Extract each inline component
|
|
|
|
For each inline component definition, create a file
|
|
`src/components/<feature>/ComponentName.tsx`.
|
|
|
|
**Import rules for extracted components:**
|
|
- Zustand hooks (e.g. `useSettings`) import from `../../store/settings` —
|
|
these are valid hook calls inside a component
|
|
- Pure helper functions import from `../../lib/<feature>` (or directly from
|
|
`../../lib/roEvents` etc. if the helper wasn't moved)
|
|
- Types import from `../../types`
|
|
- Lucide icons import from `lucide-react`
|
|
- Sibling components import from `./ComponentName`
|
|
|
|
**StatusBadge pattern** — a component that takes a `config` prop AND/or
|
|
derives it from settings:
|
|
```tsx
|
|
import { useSettings } from '../../store/settings';
|
|
import { getStatusConfig } from '../../lib/status';
|
|
|
|
export function StatusBadge({ status, config }: { ... }) { ... }
|
|
|
|
// Convenience wrapper deriving config from settings
|
|
export function StatusBadgeAuto({ status }: { status: string }) {
|
|
return <StatusBadge status={status} config={getStatusConfig(useSettings())} />;
|
|
}
|
|
```
|
|
|
|
**Memo'd components** — keep `memo()` wrapping when you extract:
|
|
```tsx
|
|
import { memo } from 'react';
|
|
export const FragmentRow = memo(function FragmentRow({ ... }) { ... });
|
|
```
|
|
|
|
**Hook calls inside extracted components:** `useSettings()`, `useNavigate()`,
|
|
etc. are valid when called inside a component that's rendered in a React tree.
|
|
The `useSettings()` call in `getStatusConfig(useSettings())` pattern is fine
|
|
at the JSX expression level — it's called on every render of the parent, and
|
|
since the parent is memo'd, that only happens on prop changes.
|
|
|
|
### Phase 5 — Create `src/components/<feature>/index.ts` (barrel)
|
|
|
|
Re-export everything the main file needs:
|
|
|
|
```typescript
|
|
export type { TabId, SavedView } from './types';
|
|
export { TABS, loadSavedViews, persistSavedViews } from './types';
|
|
export { StatusBadge } from './StatusBadge';
|
|
export { FragmentRow } from './FragmentRow';
|
|
export { CompletedRow } from './CompletedRow';
|
|
// ... all other components
|
|
export { ROModal } from './ROModal';
|
|
```
|
|
|
|
### Phase 6 — Rewrite the main file
|
|
|
|
Replace all inline definitions with imports from the barrel and lib:
|
|
|
|
```typescript
|
|
// Before: inline imports + 100s of lines of inline definitions
|
|
import { ... } from '../lib/...';
|
|
// ... 1700 lines of inline types, helpers, components, JSX
|
|
|
|
// After: imports from new locations
|
|
import {
|
|
type TabId, TABS, TabButton, FragmentRow, CompletedRow, ROModal,
|
|
} from '../components/repairOrders';
|
|
import {
|
|
normalizeRO, sortOrders, displayCustomerName,
|
|
} from '../lib/repairOrders';
|
|
// ... ~600 lines of main component logic + JSX
|
|
```
|
|
|
|
**Remove unused imports** — icons and types that were only used by now-extracted
|
|
components will be flagged as unused. Remove them from the main file's imports.
|
|
|
|
### Phase 7 — Verify
|
|
|
|
Run all three verification commands:
|
|
|
|
```
|
|
npm run build → must pass (0 errors)
|
|
npm run lint → must have 0 new warnings (pre-existing warnings are fine)
|
|
npm run test → must be identical to before (0 regressions)
|
|
```
|
|
|
|
If lint complains:
|
|
- Remove unused imports flagged by the linter
|
|
- Fix duplicate dependency array entries
|
|
- Add missing dependencies to useCallback/useEffect arrays (from hooks that now have fewer closure captures)
|
|
- Re-run `npm run lint` and `npm run build` until clean
|
|
|
|
## Pitfalls
|
|
|
|
- **Relative import paths change.** A helper at `src/pages/RepairOrders.tsx`
|
|
importing from `../lib/roEvents` when moved to `src/lib/repairOrders.ts`
|
|
must import from `./roEvents`. Check every import.
|
|
|
|
- **Lib importing from components (circular).** If a lib function needs a type
|
|
from `src/components/<feature>/types.ts`, the lib shouldn't import from
|
|
components. Instead, define the type directly in the lib file and have the
|
|
component's types.ts duplicate or re-export it.
|
|
|
|
- **Hook calls inline in JSX.** `getStatusConfig(useSettings())` inside a
|
|
JSX expression is fine in an extracted component file — it's still a valid
|
|
hook call. The convention is unusual but works with React's rules of hooks
|
|
because it's called in the same order every render.
|
|
|
|
- **Public exports.** If the main file re-exports types (`export type { Foo } from './...'`),
|
|
preserve the re-export path on the new index.ts so all importers keep working.
|
|
|
|
- **Unused icons from lucide-react.** After extraction, check which icons the
|
|
main file still needs with `search_files`. Remove unused icon imports.
|
|
|
|
- **FragmentRow/CompletedRow already memo'd.** These just move file
|
|
locations — keep the `memo()` wrapping intact.
|
|
|
|
- **Don't reorder imports or reformat.** Changing import order or whitespace
|
|
in the main file adds noise to the diff. Only touch the lines that need to
|
|
change.
|
|
|
|
## Verification Checklist
|
|
|
|
- [ ] `npm run build` passes (0 errors)
|
|
- [ ] `npm run lint` shows 0 new warnings
|
|
- [ ] `npm run test` — all tests pass, same count as before
|
|
- [ ] Main file line count reduced by 40-70%
|
|
- [ ] All inline components moved to individual files
|
|
- [ ] All pure helpers moved to `src/lib/<feature>.ts`
|
|
- [ ] Types/constants moved to `src/components/<feature>/types.ts`
|
|
- [ ] Barrel index.ts created
|
|
- [ ] No unused imports in main file
|
|
- [ ] No duplicate deps in dependency arrays
|
|
- [ ] No lint warnings specific to the new code
|
|
|
|
## Related
|
|
|
|
- `simplify-code` — for parallel review of recent refactoring changes (use
|
|
*after* extraction to find cross-file duplication)
|
|
- `plan` — for writing a step plan before starting a large extraction
|
|
- `subagent-driven-development` — for dispatching extraction tasks to
|
|
subagents in parallel
|