Files
hermes-config/skills/software-development/react-component-extraction/SKILL.md
T
2026-07-12 10:17:17 -04:00

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