@pageai/ralph-loop 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agent/PROMPT.md +58 -0
- package/.agent/STEERING.md +3 -0
- package/.agent/logs/LOG.md +13 -0
- package/.agent/prd/.gitkeep +0 -0
- package/.agent/screenshots/.gitkeep +0 -0
- package/.agent/skills/component-refactoring/SKILL.md +247 -0
- package/.agent/skills/component-refactoring/references/complexity-patterns.md +485 -0
- package/.agent/skills/component-refactoring/references/component-splitting.md +419 -0
- package/.agent/skills/component-refactoring/references/hook-extraction.md +317 -0
- package/.agent/skills/e2e-tester/SKILL.md +595 -0
- package/.agent/skills/frontend-code-review/SKILL.md +73 -0
- package/.agent/skills/frontend-code-review/references/code-quality.md +28 -0
- package/.agent/skills/frontend-code-review/references/performance.md +36 -0
- package/.agent/skills/frontend-testing/SKILL.md +316 -0
- package/.agent/skills/frontend-testing/assets/component-test.template.tsx +293 -0
- package/.agent/skills/frontend-testing/assets/hook-test.template.ts +207 -0
- package/.agent/skills/frontend-testing/assets/utility-test.template.ts +154 -0
- package/.agent/skills/frontend-testing/references/async-testing.md +345 -0
- package/.agent/skills/frontend-testing/references/checklist.md +188 -0
- package/.agent/skills/frontend-testing/references/common-patterns.md +449 -0
- package/.agent/skills/frontend-testing/references/mocking.md +289 -0
- package/.agent/skills/frontend-testing/references/workflow.md +265 -0
- package/.agent/skills/prd-creator/JSON.md +613 -0
- package/.agent/skills/prd-creator/PRD.md +196 -0
- package/.agent/skills/prd-creator/SKILL.md +143 -0
- package/.agent/skills/skill-creator/SKILL.md +355 -0
- package/.agent/skills/skill-creator/references/output-patterns.md +86 -0
- package/.agent/skills/skill-creator/references/workflows.md +28 -0
- package/.agent/skills/skill-creator/scripts/init_skill.py +300 -0
- package/.agent/skills/skill-creator/scripts/package_skill.py +110 -0
- package/.agent/skills/vercel-react-best-practices/AGENTS.md +2249 -0
- package/.agent/skills/vercel-react-best-practices/SKILL.md +125 -0
- package/.agent/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/.agent/skills/vercel-react-best-practices/rules/advanced-use-latest.md +49 -0
- package/.agent/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
- package/.agent/skills/vercel-react-best-practices/rules/async-defer-await.md +80 -0
- package/.agent/skills/vercel-react-best-practices/rules/async-dependencies.md +36 -0
- package/.agent/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
- package/.agent/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/.agent/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +59 -0
- package/.agent/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
- package/.agent/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/.agent/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/.agent/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
- package/.agent/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
- package/.agent/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
- package/.agent/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +82 -0
- package/.agent/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
- package/.agent/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
- package/.agent/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
- package/.agent/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
- package/.agent/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
- package/.agent/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/.agent/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
- package/.agent/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
- package/.agent/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
- package/.agent/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/.agent/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/.agent/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
- package/.agent/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/.agent/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/.agent/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/.agent/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/.agent/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/.agent/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/.agent/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/.agent/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
- package/.agent/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
- package/.agent/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/.agent/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/.agent/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
- package/.agent/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
- package/.agent/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/.agent/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
- package/.agent/skills/vercel-react-best-practices/rules/server-cache-react.md +26 -0
- package/.agent/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +79 -0
- package/.agent/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
- package/.agent/skills/vitest-best-practices/AGENTS.md +84 -0
- package/.agent/skills/vitest-best-practices/SKILL.md +130 -0
- package/.agent/skills/vitest-best-practices/references/aaa-pattern.md +260 -0
- package/.agent/skills/vitest-best-practices/references/assertions.md +393 -0
- package/.agent/skills/vitest-best-practices/references/async-testing.md +454 -0
- package/.agent/skills/vitest-best-practices/references/error-handling.md +382 -0
- package/.agent/skills/vitest-best-practices/references/organization.md +212 -0
- package/.agent/skills/vitest-best-practices/references/parameterized-tests.md +297 -0
- package/.agent/skills/vitest-best-practices/references/performance.md +528 -0
- package/.agent/skills/vitest-best-practices/references/snapshot-testing.md +483 -0
- package/.agent/skills/vitest-best-practices/references/test-doubles.md +499 -0
- package/.agent/skills/vitest-best-practices/references/vitest-features.md +529 -0
- package/.agent/skills/web-design-guidelines/SKILL.md +39 -0
- package/.agent/tasks/.gitkeep +0 -0
- package/.agent/tasks.json +1 -0
- package/.claude/agents/code-reviewer.md +172 -0
- package/.claude/commands/aw.md +50 -0
- package/.claude/hooks/play-sound.js +87 -0
- package/.claude/hooks/pre-tool-use.js +40 -0
- package/.claude/settings.json +54 -0
- package/.claude/settings.local.json +13 -0
- package/.mcp.json +31 -0
- package/AGENTS.md +44 -0
- package/CLAUDE.md +1 -0
- package/README.md +236 -0
- package/bin/cli.js +156 -0
- package/bin/lib/copy.js +149 -0
- package/bin/lib/display.js +137 -0
- package/package.json +65 -0
- package/ralph.sh +333 -0
- package/scripts/lib/args.sh +44 -0
- package/scripts/lib/cleanup.sh +53 -0
- package/scripts/lib/constants.sh +25 -0
- package/scripts/lib/display.sh +196 -0
- package/scripts/lib/logging.sh +30 -0
- package/scripts/lib/notify.sh +41 -0
- package/scripts/lib/output.sh +147 -0
- package/scripts/lib/preflight.sh +57 -0
- package/scripts/lib/preview.sh +77 -0
- package/scripts/lib/promise.sh +76 -0
- package/scripts/lib/spinner.sh +85 -0
- package/scripts/lib/terminal.sh +57 -0
- package/scripts/lib/timing.sh +223 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
# Component Splitting Patterns
|
|
2
|
+
|
|
3
|
+
This document provides detailed guidance on splitting large components into smaller, focused components.
|
|
4
|
+
|
|
5
|
+
## When to Split Components
|
|
6
|
+
|
|
7
|
+
Split a component when you identify:
|
|
8
|
+
|
|
9
|
+
1. **Multiple UI sections** - Distinct visual areas with minimal coupling that can be composed independently
|
|
10
|
+
1. **Conditional rendering blocks** - Large `{condition && <JSX />}` blocks
|
|
11
|
+
1. **Repeated patterns** - Similar UI structures used multiple times
|
|
12
|
+
1. **300+ lines** - Component exceeds manageable size
|
|
13
|
+
1. **Modal clusters** - Multiple modals rendered in one component
|
|
14
|
+
|
|
15
|
+
## Splitting Strategies
|
|
16
|
+
|
|
17
|
+
### Strategy 1: Section-Based Splitting
|
|
18
|
+
|
|
19
|
+
Identify visual sections and extract each as a component.
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// ❌ Before: Monolithic component (500+ lines)
|
|
23
|
+
const ConfigurationPage = () => {
|
|
24
|
+
return (
|
|
25
|
+
<div>
|
|
26
|
+
{/* Header Section - 50 lines */}
|
|
27
|
+
<div className="header">
|
|
28
|
+
<h1>{t('configuration.title')}</h1>
|
|
29
|
+
<div className="actions">
|
|
30
|
+
{isAdvancedMode && <Badge>Advanced</Badge>}
|
|
31
|
+
<ModelParameterModal ... />
|
|
32
|
+
<AppPublisher ... />
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
{/* Config Section - 200 lines */}
|
|
37
|
+
<div className="config">
|
|
38
|
+
<Config />
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
{/* Debug Section - 150 lines */}
|
|
42
|
+
<div className="debug">
|
|
43
|
+
<Debug ... />
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
{/* Modals Section - 100 lines */}
|
|
47
|
+
{showSelectDataSet && <SelectDataSet ... />}
|
|
48
|
+
{showHistoryModal && <EditHistoryModal ... />}
|
|
49
|
+
{showUseGPT4Confirm && <Confirm ... />}
|
|
50
|
+
</div>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ✅ After: Split into focused components
|
|
55
|
+
// configuration/
|
|
56
|
+
// ├── index.tsx (orchestration)
|
|
57
|
+
// ├── configuration-header.tsx
|
|
58
|
+
// ├── configuration-content.tsx
|
|
59
|
+
// ├── configuration-debug.tsx
|
|
60
|
+
// └── configuration-modals.tsx
|
|
61
|
+
|
|
62
|
+
// configuration-header.tsx
|
|
63
|
+
interface ConfigurationHeaderProps {
|
|
64
|
+
isAdvancedMode: boolean
|
|
65
|
+
onPublish: () => void
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const ConfigurationHeader: FC<ConfigurationHeaderProps> = ({
|
|
69
|
+
isAdvancedMode,
|
|
70
|
+
onPublish,
|
|
71
|
+
}) => {
|
|
72
|
+
const { t } = useTranslation()
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div className="header">
|
|
76
|
+
<h1>{t('configuration.title')}</h1>
|
|
77
|
+
<div className="actions">
|
|
78
|
+
{isAdvancedMode && <Badge>Advanced</Badge>}
|
|
79
|
+
<ModelParameterModal ... />
|
|
80
|
+
<AppPublisher onPublish={onPublish} />
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// index.tsx (orchestration only)
|
|
87
|
+
const ConfigurationPage = () => {
|
|
88
|
+
const { modelConfig, setModelConfig } = useModelConfig()
|
|
89
|
+
const { activeModal, openModal, closeModal } = useModalState()
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<div>
|
|
93
|
+
<ConfigurationHeader
|
|
94
|
+
isAdvancedMode={isAdvancedMode}
|
|
95
|
+
onPublish={handlePublish}
|
|
96
|
+
/>
|
|
97
|
+
<ConfigurationContent
|
|
98
|
+
modelConfig={modelConfig}
|
|
99
|
+
onConfigChange={setModelConfig}
|
|
100
|
+
/>
|
|
101
|
+
{!isMobile && (
|
|
102
|
+
<ConfigurationDebug
|
|
103
|
+
inputs={inputs}
|
|
104
|
+
onSetting={handleSetting}
|
|
105
|
+
/>
|
|
106
|
+
)}
|
|
107
|
+
<ConfigurationModals
|
|
108
|
+
activeModal={activeModal}
|
|
109
|
+
onClose={closeModal}
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Strategy 2: Conditional Block Extraction
|
|
117
|
+
|
|
118
|
+
Extract large conditional rendering blocks.
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
// ❌ Before: Large conditional blocks
|
|
122
|
+
const AppInfo = () => {
|
|
123
|
+
return (
|
|
124
|
+
<div>
|
|
125
|
+
{expand ? (
|
|
126
|
+
<div className="expanded">
|
|
127
|
+
{/* 100 lines of expanded view */}
|
|
128
|
+
</div>
|
|
129
|
+
) : (
|
|
130
|
+
<div className="collapsed">
|
|
131
|
+
{/* 50 lines of collapsed view */}
|
|
132
|
+
</div>
|
|
133
|
+
)}
|
|
134
|
+
</div>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ✅ After: Separate view components
|
|
139
|
+
const AppInfoExpanded: FC<AppInfoViewProps> = ({ appDetail, onAction }) => {
|
|
140
|
+
return (
|
|
141
|
+
<div className="expanded">
|
|
142
|
+
{/* Clean, focused expanded view */}
|
|
143
|
+
</div>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const AppInfoCollapsed: FC<AppInfoViewProps> = ({ appDetail, onAction }) => {
|
|
148
|
+
return (
|
|
149
|
+
<div className="collapsed">
|
|
150
|
+
{/* Clean, focused collapsed view */}
|
|
151
|
+
</div>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const AppInfo = () => {
|
|
156
|
+
return (
|
|
157
|
+
<div>
|
|
158
|
+
{expand
|
|
159
|
+
? <AppInfoExpanded appDetail={appDetail} onAction={handleAction} />
|
|
160
|
+
: <AppInfoCollapsed appDetail={appDetail} onAction={handleAction} />
|
|
161
|
+
}
|
|
162
|
+
</div>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Strategy 3: Modal Extraction
|
|
168
|
+
|
|
169
|
+
Extract modals with their trigger logic.
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
// ❌ Before: Multiple modals in one component
|
|
173
|
+
const AppInfo = () => {
|
|
174
|
+
const [showEdit, setShowEdit] = useState(false)
|
|
175
|
+
const [showDuplicate, setShowDuplicate] = useState(false)
|
|
176
|
+
const [showDelete, setShowDelete] = useState(false)
|
|
177
|
+
const [showSwitch, setShowSwitch] = useState(false)
|
|
178
|
+
|
|
179
|
+
const onEdit = async (data) => { /* 20 lines */ }
|
|
180
|
+
const onDuplicate = async (data) => { /* 20 lines */ }
|
|
181
|
+
const onDelete = async () => { /* 15 lines */ }
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<div>
|
|
185
|
+
{/* Main content */}
|
|
186
|
+
|
|
187
|
+
{showEdit && <EditModal onConfirm={onEdit} onClose={() => setShowEdit(false)} />}
|
|
188
|
+
{showDuplicate && <DuplicateModal onConfirm={onDuplicate} onClose={() => setShowDuplicate(false)} />}
|
|
189
|
+
{showDelete && <DeleteConfirm onConfirm={onDelete} onClose={() => setShowDelete(false)} />}
|
|
190
|
+
{showSwitch && <SwitchModal ... />}
|
|
191
|
+
</div>
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ✅ After: Modal manager component
|
|
196
|
+
// app-info-modals.tsx
|
|
197
|
+
type ModalType = 'edit' | 'duplicate' | 'delete' | 'switch' | null
|
|
198
|
+
|
|
199
|
+
interface AppInfoModalsProps {
|
|
200
|
+
appDetail: AppDetail
|
|
201
|
+
activeModal: ModalType
|
|
202
|
+
onClose: () => void
|
|
203
|
+
onSuccess: () => void
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const AppInfoModals: FC<AppInfoModalsProps> = ({
|
|
207
|
+
appDetail,
|
|
208
|
+
activeModal,
|
|
209
|
+
onClose,
|
|
210
|
+
onSuccess,
|
|
211
|
+
}) => {
|
|
212
|
+
const handleEdit = async (data) => { /* logic */ }
|
|
213
|
+
const handleDuplicate = async (data) => { /* logic */ }
|
|
214
|
+
const handleDelete = async () => { /* logic */ }
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<>
|
|
218
|
+
{activeModal === 'edit' && (
|
|
219
|
+
<EditModal
|
|
220
|
+
appDetail={appDetail}
|
|
221
|
+
onConfirm={handleEdit}
|
|
222
|
+
onClose={onClose}
|
|
223
|
+
/>
|
|
224
|
+
)}
|
|
225
|
+
{activeModal === 'duplicate' && (
|
|
226
|
+
<DuplicateModal
|
|
227
|
+
appDetail={appDetail}
|
|
228
|
+
onConfirm={handleDuplicate}
|
|
229
|
+
onClose={onClose}
|
|
230
|
+
/>
|
|
231
|
+
)}
|
|
232
|
+
{activeModal === 'delete' && (
|
|
233
|
+
<DeleteConfirm
|
|
234
|
+
onConfirm={handleDelete}
|
|
235
|
+
onClose={onClose}
|
|
236
|
+
/>
|
|
237
|
+
)}
|
|
238
|
+
{activeModal === 'switch' && (
|
|
239
|
+
<SwitchModal
|
|
240
|
+
appDetail={appDetail}
|
|
241
|
+
onClose={onClose}
|
|
242
|
+
/>
|
|
243
|
+
)}
|
|
244
|
+
</>
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Parent component
|
|
249
|
+
const AppInfo = () => {
|
|
250
|
+
const { activeModal, openModal, closeModal } = useModalState()
|
|
251
|
+
|
|
252
|
+
return (
|
|
253
|
+
<div>
|
|
254
|
+
{/* Main content with openModal triggers */}
|
|
255
|
+
<Button onClick={() => openModal('edit')}>Edit</Button>
|
|
256
|
+
|
|
257
|
+
<AppInfoModals
|
|
258
|
+
appDetail={appDetail}
|
|
259
|
+
activeModal={activeModal}
|
|
260
|
+
onClose={closeModal}
|
|
261
|
+
onSuccess={handleSuccess}
|
|
262
|
+
/>
|
|
263
|
+
</div>
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Strategy 4: List Item Extraction
|
|
269
|
+
|
|
270
|
+
Extract repeated item rendering.
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
// ❌ Before: Inline item rendering
|
|
274
|
+
const OperationsList = () => {
|
|
275
|
+
return (
|
|
276
|
+
<div>
|
|
277
|
+
{operations.map(op => (
|
|
278
|
+
<div key={op.id} className="operation-item">
|
|
279
|
+
<span className="icon">{op.icon}</span>
|
|
280
|
+
<span className="title">{op.title}</span>
|
|
281
|
+
<span className="description">{op.description}</span>
|
|
282
|
+
<button onClick={() => op.onClick()}>
|
|
283
|
+
{op.actionLabel}
|
|
284
|
+
</button>
|
|
285
|
+
{op.badge && <Badge>{op.badge}</Badge>}
|
|
286
|
+
{/* More complex rendering... */}
|
|
287
|
+
</div>
|
|
288
|
+
))}
|
|
289
|
+
</div>
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ✅ After: Extracted item component
|
|
294
|
+
interface OperationItemProps {
|
|
295
|
+
operation: Operation
|
|
296
|
+
onAction: (id: string) => void
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const OperationItem: FC<OperationItemProps> = ({ operation, onAction }) => {
|
|
300
|
+
return (
|
|
301
|
+
<div className="operation-item">
|
|
302
|
+
<span className="icon">{operation.icon}</span>
|
|
303
|
+
<span className="title">{operation.title}</span>
|
|
304
|
+
<span className="description">{operation.description}</span>
|
|
305
|
+
<button onClick={() => onAction(operation.id)}>
|
|
306
|
+
{operation.actionLabel}
|
|
307
|
+
</button>
|
|
308
|
+
{operation.badge && <Badge>{operation.badge}</Badge>}
|
|
309
|
+
</div>
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const OperationsList = () => {
|
|
314
|
+
const handleAction = useCallback((id: string) => {
|
|
315
|
+
const op = operations.find(o => o.id === id)
|
|
316
|
+
op?.onClick()
|
|
317
|
+
}, [operations])
|
|
318
|
+
|
|
319
|
+
return (
|
|
320
|
+
<div>
|
|
321
|
+
{operations.map(op => (
|
|
322
|
+
<OperationItem
|
|
323
|
+
key={op.id}
|
|
324
|
+
operation={op}
|
|
325
|
+
onAction={handleAction}
|
|
326
|
+
/>
|
|
327
|
+
))}
|
|
328
|
+
</div>
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
## Directory Structure Patterns
|
|
334
|
+
|
|
335
|
+
Feature-Based Structure (Standard) is recommended. Group related components of a feature together and use a domain-driven approach for file naming and structure.
|
|
336
|
+
|
|
337
|
+
## Props Design
|
|
338
|
+
|
|
339
|
+
### Minimal Props Principle
|
|
340
|
+
|
|
341
|
+
Pass only what's needed:
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
// ❌ Bad: Passing entire objects when only some fields needed
|
|
345
|
+
<ConfigHeader appDetail={appDetail} modelConfig={modelConfig} />
|
|
346
|
+
|
|
347
|
+
// ✅ Good: Destructure to minimum required
|
|
348
|
+
<ConfigHeader
|
|
349
|
+
appName={appDetail.name}
|
|
350
|
+
isAdvancedMode={modelConfig.isAdvanced}
|
|
351
|
+
onPublish={handlePublish}
|
|
352
|
+
/>
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### Callback Props Pattern
|
|
356
|
+
|
|
357
|
+
Use callbacks for child-to-parent communication:
|
|
358
|
+
|
|
359
|
+
```typescript
|
|
360
|
+
// Parent
|
|
361
|
+
const Parent = () => {
|
|
362
|
+
const [value, setValue] = useState('')
|
|
363
|
+
|
|
364
|
+
return (
|
|
365
|
+
<Child
|
|
366
|
+
value={value}
|
|
367
|
+
onChange={setValue}
|
|
368
|
+
onSubmit={handleSubmit}
|
|
369
|
+
/>
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Child
|
|
374
|
+
interface ChildProps {
|
|
375
|
+
value: string
|
|
376
|
+
onChange: (value: string) => void
|
|
377
|
+
onSubmit: () => void
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const Child: FC<ChildProps> = ({ value, onChange, onSubmit }) => {
|
|
381
|
+
return (
|
|
382
|
+
<div>
|
|
383
|
+
<input value={value} onChange={e => onChange(e.target.value)} />
|
|
384
|
+
<button onClick={onSubmit}>Submit</button>
|
|
385
|
+
</div>
|
|
386
|
+
)
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### Render Props for Flexibility
|
|
391
|
+
|
|
392
|
+
When sub-components need parent context:
|
|
393
|
+
|
|
394
|
+
```typescript
|
|
395
|
+
interface ListProps<T> {
|
|
396
|
+
items: T[]
|
|
397
|
+
renderItem: (item: T, index: number) => React.ReactNode
|
|
398
|
+
renderEmpty?: () => React.ReactNode
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function List<T>({ items, renderItem, renderEmpty }: ListProps<T>) {
|
|
402
|
+
if (items.length === 0 && renderEmpty) {
|
|
403
|
+
return <>{renderEmpty()}</>
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return (
|
|
407
|
+
<div>
|
|
408
|
+
{items.map((item, index) => renderItem(item, index))}
|
|
409
|
+
</div>
|
|
410
|
+
)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Usage
|
|
414
|
+
<List
|
|
415
|
+
items={operations}
|
|
416
|
+
renderItem={(op, i) => <OperationItem key={i} operation={op} />}
|
|
417
|
+
renderEmpty={() => <EmptyState message="No operations" />}
|
|
418
|
+
/>
|
|
419
|
+
```
|