9.1 KiB
name, description, version, author, license, platforms, metadata
| name | description | version | author | license | platforms | metadata | |||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| react-component-extraction | 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. | 1.0.0 | Hermes Agent | MIT |
|
|
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 componentsfunction helperName— pure helpers (no hooks, no JSX)interface/type— type aliasesconst/letat module scope — constantsimportlines — 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)
// 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).
// 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/roEventsetc. 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:
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:
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:
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:
// 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 lintandnpm run builduntil clean
Pitfalls
-
Relative import paths change. A helper at
src/pages/RepairOrders.tsximporting from../lib/roEventswhen moved tosrc/lib/repairOrders.tsmust 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 buildpasses (0 errors)npm run lintshows 0 new warningsnpm 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 extractionsubagent-driven-development— for dispatching extraction tasks to subagents in parallel