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

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
linux
macos
hermes
tags related_skills
react
refactor
component-extraction
mega-component
barrel
typescript
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)
// 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/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:

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 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
  • 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