initial commit
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
---
|
||||
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
|
||||
Reference in New Issue
Block a user