--- 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//` 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/.ts` | | Type aliases, constants, localStorage helpers | `src/components//types.ts` | | Inline component definitions (JSX + hooks) | `src/components//ComponentName.tsx` | | Barrel re-exports | `src/components//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/.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//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//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/` (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 ; } ``` **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//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//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/.ts` - [ ] Types/constants moved to `src/components//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