| name | eisland-dev-refactor-module-split |
| author | JNTMTMTM |
| description | Refactor a monolithic React component file into a standardized module structure with components/, hooks/, utils/, types/, and config/ subdirectories. Use this skill whenever the user asks to "split", "refactor", "ๆๅ", "ๆ่งฃ", or "restructure" a component file into subdirectories, or when they mention organizing code into modules.
|
Refactor Module Split
Split a monolithic React component file into a clean module structure with five subdirectories: components/, hooks/, utils/, types/, and config/, plus an index.ts entry point.
When to use
- A single
.tsx file has grown large and contains multiple concerns (utility functions, hooks, sub-components, constants, type definitions)
- The user explicitly asks to split/refactor a component into subdirectories
- The file is a React component that mixes UI rendering, state management, utility logic, and configuration
Process
Step 0: Ensure module scaffolding exists
Before extracting any code, verify the target module directory has the required structure. If any of the following are missing, create them first:
<module>/
โโโ index.ts (if missing, create with placeholder export)
โโโ types/ (if missing, create directory)
โโโ config/ (if missing, create directory)
โโโ utils/ (if missing, create directory)
โโโ hooks/ (if missing, create directory)
โโโ components/ (if missing, create directory)
- Empty directories should contain a
.gitkeep placeholder
index.ts should initially export the original component (update after refactoring)
Step 1: Analyze the source file
Read the target file in full. Categorize every piece of code into one of five buckets:
| Bucket | Criteria | Target directory |
|---|
| Types | type, interface, component prop interfaces, hook return types | types/ |
| Constants | const values, config keys, store keys, i18n keys/defaults | config/ |
| Pure functions | No React hooks, no side effects, deterministic inputโoutput | utils/ |
| React hooks | Uses useState, useEffect, useMemo, useCallback, or custom hooks | hooks/ |
| Sub-components | Returns JSX, used within the main component | components/ |
Also check if the file's sibling directories already exist (e.g., components/ or hooks/ may already have files from prior refactoring). Don't duplicate existing extractions.
Step 2: Create the extracted files
For each extracted piece, create a new file in the appropriate directory. Every file must include:
- License header โ copy the exact GPL-3.0 block from the source file
- File-level JSDoc โ
@file, @description, @author tags
- Function/type JSDoc โ
@param, @returns for all exported functions
- Correct imports โ relative paths back to shared types or store slices
Naming conventions:
types/ โ camelCase with module prefix (e.g., todoTypes.ts, albumTypes.ts, localFileSearchTypes.ts)
config/ โ camelCase with module prefix (e.g., todoConfig.ts, albumConfig.ts, localFileSearchConfig.ts)
utils/ โ camelCase with module prefix (e.g., todoUtils.ts, albumUtils.ts, localFileSearchUtils.ts)
hooks/ โ camelCase with use prefix (e.g., useTodos.ts, useAlbumItems.ts, useLocalFileSearch.ts)
components/ โ PascalCase component name (e.g., TodoTab.tsx, TodoHeader.tsx, AlbumGridItem.tsx)
Step 3: Define types first
Create types/<module>Types.ts before other files. This file must contain:
- Domain types โ data models, enums, union types used by the module
- Hook return type โ
Use<Module>Return interface describing the hook's full return shape
- Component prop interfaces โ
<Component>Props for every sub-component
All component prop interfaces and hook return types MUST be defined in types/, not inline in component or hook files.
Example pattern:
export type Priority = 'P0' | 'P1' | 'P2';
export interface TodoItem { ... }
export interface UseTodosReturn { ... }
export interface TodoHeaderProps { ... }
export interface TodoInputBarProps { ... }
Step 4: Rewrite the source file
Replace the extracted code in the original file with imports from the new modules. The source file should become a thin composition layer that:
- Imports hook from
../hooks/use<Module>
- Imports sub-components from
./<ComponentName>
- Calls the hook at the top level
- Destructures hook return values and passes them as props to sub-components
- Contains no extracted logic, no
useEffect, no utility functions
Step 5: Create the index.ts entry point
Create <module>/index.ts that re-exports the main component:
export { TodoTab } from './components/TodoTab';
Step 6: Verify
Run these checks in order โ all must pass before committing:
npx tsc --noEmit --pretty
npm run comment:check
npm run i18n:check
npm run test
If any check fails, fix the issue before proceeding. Common failures:
comment:check โ missing license header, missing @file/@description/@author, missing JSDoc on exported functions
i18n:check โ a t('key') call references a key not present in both locale files
test โ a refactored import path broke a test, or an extracted function changed behavior
Step 7: Commit
Use a conventional commit message:
refactor(<module-name>): extract types, utils, hooks, components, config from <OriginalFile>
- types/<name>Types.ts: type definitions + component prop interfaces
- utils/<name>Utils.ts: <what it does>
- hooks/use<Name>.ts: <what it does>
- components/<Name>.tsx: <what it does>
- config/<name>Config.ts: <what it contains>
- index.ts: module entry point
- <OriginalFile>.ts: simplified from <N> to <M> lines
Directory structure
feature/
โโโ index.ts (module entry point, re-exports main component)
โโโ types/
โ โโโ featureTypes.ts (types, hook return type, component prop interfaces)
โโโ config/
โ โโโ featureConfig.ts (constants, store keys, defaults)
โโโ utils/
โ โโโ featureUtils.ts (pure utility functions)
โโโ hooks/
โ โโโ useFeature.ts (all state management logic)
โโโ components/
โโโ FeatureTab.tsx (thin composition layer: hook + sub-components)
โโโ FeatureHeader.tsx (sub-component)
โโโ FeatureList.tsx (sub-component)
Real examples from the codebase
todo/
โโโ index.ts
โโโ types/todoTypes.ts
โโโ config/todoConfig.ts
โโโ utils/todoUtils.ts
โโโ hooks/useTodos.ts
โโโ components/
โโโ TodoTab.tsx
โโโ TodoHeader.tsx
โโโ TodoInputBar.tsx
โโโ TodoList.tsx
โโโ TodoItem.tsx
โโโ TodoSubItem.tsx
album/
โโโ index.ts
โโโ types/albumTypes.ts
โโโ config/albumConfig.ts
โโโ utils/albumUtils.ts
โโโ hooks/
โ โโโ useAlbumItems.ts
โ โโโ useAlbumViewer.ts
โ โโโ useAlbumViewerActions.ts
โ โโโ useAlbumSelection.ts
โ โโโ useAlbumGridConfig.ts
โ โโโ useAlbumDrag.ts
โโโ components/
โโโ AlbumTab.tsx
โโโ AlbumHeader.tsx
โโโ AlbumOverview.tsx
โโโ AlbumViewer.tsx
โโโ AlbumGridItem.tsx
โโโ AlbumMetaPanel.tsx
โโโ AlbumSelectionBar.tsx
localFileSearch/
โโโ index.ts
โโโ types/localFileSearchTypes.ts
โโโ config/localFileSearchConfig.ts
โโโ utils/localFileSearchUtils.ts
โโโ hooks/useLocalFileSearch.ts
โโโ components/
โโโ LocalFileSearchTab.tsx
โโโ LocalFileSearchHeader.tsx
โโโ LocalFileSearchRootRow.tsx
โโโ LocalFileSearchQueryRow.tsx
โโโ LocalFileSearchConfigPanel.tsx
โโโ LocalFileSearchResults.tsx
Acceptance criteria
Every extracted file MUST satisfy ALL of the following before the refactoring is considered complete.
Comment standards (references/COMMENT_STANDARDS.md)
| # | Criterion | Check |
|---|
| C1 | Every .ts/.tsx file starts with the GPL-3.0 license block (project name, URL, copyright, author, GPL notice) | npm run comment:check |
| C2 | Every .ts/.tsx file has a file-level JSDoc with @file, @description, @author | npm run comment:check |
| C3 | Every exported function/class/method has JSDoc with @param and @returns | npm run comment:check |
| C4 | @author is set to ้ธกๅฅ (project default) | Visual check |
| C5 | Comments are in Chinese, explain "why" not "what" | Visual check |
| C6 | No comments on simple getters/setters, self-explanatory assignments, or template code | Visual check |
Frontend standards (references/FRONTEND_STANDARDS.md)
| # | Criterion | Check |
|---|
| F1 | Use const for constants, let for reassignable vars, never var | npm run test (lint) |
| F2 | One variable per declaration | Visual check |
| F3 | No any type โ use explicit types or unknown | npx tsc --noEmit |
| F4 | Interface over type for object shapes; type only for unions/tuples | Visual check |
| F5 | ES6 modules (import/export), no require() | Visual check |
| F6 | Import order: builtins โ external โ internal (absolute) โ parent relative โ sibling relative | Visual check |
| F7 | No file extension in import paths | Visual check |
| F8 | Single quotes for JS/TS strings, double quotes for JSX attribute values | Visual check |
| F9 | 2-space indentation | Visual check |
| F10 | Semicolons at end of statements | Visual check |
| F11 | Unix line endings (\n) | Visual check |
| F12 | Variables/functions: camelCase; Classes/interfaces/types/enums: PascalCase; Constants: UPPER_SNAKE_CASE | Visual check |
| F13 | React components defined as named function declarations, not anonymous arrow functions | Visual check |
| F14 | No unused React import (JSX transform handles it) | npx tsc --noEmit |
| F15 | Hooks called only at top level, never in conditions/loops | Visual check |
i18n completeness
| # | Criterion | Check |
|---|
| I1 | Every t('key') in extracted files has a matching entry in both zh-CN.json and en-US.json | npm run i18n:check |
| I2 | No hardcoded Chinese or English strings in UI code โ all wrapped in t() | npm run i18n:check |
Behavioral preservation
| # | Criterion | Check |
|---|
| B1 | TypeScript compiles with zero errors | npx tsc --noEmit |
| B2 | All existing tests pass | npm run test |
| B3 | No logic changes โ the refactored code produces identical behavior | npm run test |
| B4 | Every useEffect extracted to a hook preserves the same dependency array | Visual check against original |
Important rules
- All five directories must exist (
types/, config/, utils/, hooks/, components/). If a directory has no files, create a .gitkeep placeholder.
types/ must be created first. All component prop interfaces and hook return types go in types/, not inline in component or hook files.
index.ts is required. Every module must have an index.ts that re-exports the main component.
- Extract ALL hooks. Every
useEffect, useState, useMemo, useCallback block in the original file must end up in a hook file. The main component should have zero useEffect calls after refactoring.
- Extract ALL pure functions. If a function has no React hooks and no side effects, it belongs in
utils/. Don't leave pure functions in the component.
- Extract ALL types. All
type and interface definitions used across multiple files go in types/.
- Use
CSSProperties not React.CSSProperties. When importing CSS type utilities, import CSSProperties directly from 'react' โ do not use React.CSSProperties without importing React.
- Match existing style. Use the same comment density, naming patterns, and import ordering as the rest of the project.
- Preserve behavior exactly. This is a pure structural refactor โ no logic changes, no new features, no "improvements" to adjacent code.
- Relative imports from subdirectories. A file in
hooks/ that needs a store type uses ../../../../store/types, not an alias.
- i18n keys stay in components. The
t() calls remain in the JSX; only the default values and key constants move to config/.
- Run all four verification commands before committing. Do not skip any check. If a check fails, fix it before committing.