devflow-kit 1.0.0 → 1.2.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/CHANGELOG.md +69 -0
- package/README.md +35 -11
- package/dist/cli.js +5 -1
- package/dist/commands/ambient.d.ts +18 -0
- package/dist/commands/ambient.js +136 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +97 -10
- package/dist/commands/memory.d.ts +22 -0
- package/dist/commands/memory.js +175 -0
- package/dist/commands/uninstall.js +72 -5
- package/dist/plugins.js +74 -3
- package/dist/utils/post-install.d.ts +12 -0
- package/dist/utils/post-install.js +82 -1
- package/dist/utils/safe-delete-install.d.ts +7 -0
- package/dist/utils/safe-delete-install.js +40 -5
- package/package.json +2 -1
- package/plugins/devflow-accessibility/.claude-plugin/plugin.json +15 -0
- package/plugins/devflow-ambient/.claude-plugin/plugin.json +7 -0
- package/plugins/devflow-ambient/README.md +49 -0
- package/plugins/devflow-ambient/commands/ambient.md +110 -0
- package/plugins/devflow-ambient/skills/ambient-router/SKILL.md +89 -0
- package/plugins/devflow-ambient/skills/ambient-router/references/skill-catalog.md +68 -0
- package/plugins/devflow-audit-claude/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-code-review/.claude-plugin/plugin.json +1 -4
- package/plugins/devflow-code-review/agents/reviewer.md +8 -0
- package/plugins/devflow-code-review/commands/code-review-teams.md +11 -1
- package/plugins/devflow-code-review/commands/code-review.md +12 -2
- package/plugins/devflow-core-skills/.claude-plugin/plugin.json +3 -6
- package/plugins/devflow-core-skills/skills/docs-framework/SKILL.md +10 -6
- package/plugins/devflow-core-skills/skills/test-driven-development/SKILL.md +139 -0
- package/plugins/devflow-core-skills/skills/test-driven-development/references/rationalization-prevention.md +111 -0
- package/plugins/devflow-debug/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-frontend-design/.claude-plugin/plugin.json +15 -0
- package/plugins/devflow-go/.claude-plugin/plugin.json +15 -0
- package/plugins/devflow-go/skills/go/SKILL.md +187 -0
- package/plugins/devflow-go/skills/go/references/concurrency.md +312 -0
- package/plugins/devflow-go/skills/go/references/detection.md +129 -0
- package/plugins/devflow-go/skills/go/references/patterns.md +232 -0
- package/plugins/devflow-go/skills/go/references/violations.md +205 -0
- package/plugins/devflow-implement/.claude-plugin/plugin.json +1 -3
- package/plugins/devflow-implement/agents/coder.md +11 -6
- package/plugins/devflow-java/.claude-plugin/plugin.json +15 -0
- package/plugins/devflow-java/skills/java/SKILL.md +183 -0
- package/plugins/devflow-java/skills/java/references/detection.md +120 -0
- package/plugins/devflow-java/skills/java/references/modern-java.md +270 -0
- package/plugins/devflow-java/skills/java/references/patterns.md +235 -0
- package/plugins/devflow-java/skills/java/references/violations.md +213 -0
- package/plugins/devflow-python/.claude-plugin/plugin.json +15 -0
- package/plugins/devflow-python/skills/python/SKILL.md +188 -0
- package/plugins/devflow-python/skills/python/references/async.md +220 -0
- package/plugins/devflow-python/skills/python/references/detection.md +128 -0
- package/plugins/devflow-python/skills/python/references/patterns.md +226 -0
- package/plugins/devflow-python/skills/python/references/violations.md +204 -0
- package/plugins/devflow-react/.claude-plugin/plugin.json +15 -0
- package/plugins/{devflow-core-skills → devflow-react}/skills/react/SKILL.md +1 -1
- package/plugins/{devflow-core-skills → devflow-react}/skills/react/references/patterns.md +3 -3
- package/plugins/devflow-resolve/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-rust/.claude-plugin/plugin.json +15 -0
- package/plugins/devflow-rust/skills/rust/SKILL.md +193 -0
- package/plugins/devflow-rust/skills/rust/references/detection.md +131 -0
- package/plugins/devflow-rust/skills/rust/references/ownership.md +242 -0
- package/plugins/devflow-rust/skills/rust/references/patterns.md +210 -0
- package/plugins/devflow-rust/skills/rust/references/violations.md +191 -0
- package/plugins/devflow-self-review/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-specify/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-typescript/.claude-plugin/plugin.json +15 -0
- package/plugins/{devflow-core-skills → devflow-typescript}/skills/typescript/references/patterns.md +3 -3
- package/scripts/hooks/ambient-prompt.sh +48 -0
- package/scripts/hooks/background-memory-update.sh +49 -8
- package/scripts/hooks/ensure-memory-gitignore.sh +17 -0
- package/scripts/hooks/pre-compact-memory.sh +12 -6
- package/scripts/hooks/session-start-memory.sh +50 -8
- package/scripts/hooks/stop-update-memory.sh +10 -6
- package/shared/agents/coder.md +11 -6
- package/shared/agents/reviewer.md +8 -0
- package/shared/skills/ambient-router/SKILL.md +89 -0
- package/shared/skills/ambient-router/references/skill-catalog.md +68 -0
- package/shared/skills/docs-framework/SKILL.md +10 -6
- package/shared/skills/go/SKILL.md +187 -0
- package/shared/skills/go/references/concurrency.md +312 -0
- package/shared/skills/go/references/detection.md +129 -0
- package/shared/skills/go/references/patterns.md +232 -0
- package/shared/skills/go/references/violations.md +205 -0
- package/shared/skills/java/SKILL.md +183 -0
- package/shared/skills/java/references/detection.md +120 -0
- package/shared/skills/java/references/modern-java.md +270 -0
- package/shared/skills/java/references/patterns.md +235 -0
- package/shared/skills/java/references/violations.md +213 -0
- package/shared/skills/python/SKILL.md +188 -0
- package/shared/skills/python/references/async.md +220 -0
- package/shared/skills/python/references/detection.md +128 -0
- package/shared/skills/python/references/patterns.md +226 -0
- package/shared/skills/python/references/violations.md +204 -0
- package/shared/skills/react/SKILL.md +1 -1
- package/shared/skills/react/references/patterns.md +3 -3
- package/shared/skills/rust/SKILL.md +193 -0
- package/shared/skills/rust/references/detection.md +131 -0
- package/shared/skills/rust/references/ownership.md +242 -0
- package/shared/skills/rust/references/patterns.md +210 -0
- package/shared/skills/rust/references/violations.md +191 -0
- package/shared/skills/test-driven-development/SKILL.md +139 -0
- package/shared/skills/test-driven-development/references/rationalization-prevention.md +111 -0
- package/shared/skills/typescript/references/patterns.md +3 -3
- package/src/templates/managed-settings.json +14 -0
- package/plugins/devflow-code-review/skills/react/SKILL.md +0 -276
- package/plugins/devflow-code-review/skills/react/references/patterns.md +0 -1331
- package/plugins/devflow-core-skills/skills/accessibility/SKILL.md +0 -229
- package/plugins/devflow-core-skills/skills/accessibility/references/detection.md +0 -171
- package/plugins/devflow-core-skills/skills/accessibility/references/patterns.md +0 -670
- package/plugins/devflow-core-skills/skills/accessibility/references/violations.md +0 -419
- package/plugins/devflow-core-skills/skills/frontend-design/SKILL.md +0 -254
- package/plugins/devflow-core-skills/skills/frontend-design/references/detection.md +0 -184
- package/plugins/devflow-core-skills/skills/frontend-design/references/patterns.md +0 -511
- package/plugins/devflow-core-skills/skills/frontend-design/references/violations.md +0 -453
- package/plugins/devflow-core-skills/skills/react/references/violations.md +0 -565
- package/plugins/devflow-implement/skills/accessibility/SKILL.md +0 -229
- package/plugins/devflow-implement/skills/accessibility/references/detection.md +0 -171
- package/plugins/devflow-implement/skills/accessibility/references/patterns.md +0 -670
- package/plugins/devflow-implement/skills/accessibility/references/violations.md +0 -419
- package/plugins/devflow-implement/skills/frontend-design/SKILL.md +0 -254
- package/plugins/devflow-implement/skills/frontend-design/references/detection.md +0 -184
- package/plugins/devflow-implement/skills/frontend-design/references/patterns.md +0 -511
- package/plugins/devflow-implement/skills/frontend-design/references/violations.md +0 -453
- /package/plugins/{devflow-code-review → devflow-accessibility}/skills/accessibility/SKILL.md +0 -0
- /package/plugins/{devflow-code-review → devflow-accessibility}/skills/accessibility/references/detection.md +0 -0
- /package/plugins/{devflow-code-review → devflow-accessibility}/skills/accessibility/references/patterns.md +0 -0
- /package/plugins/{devflow-code-review → devflow-accessibility}/skills/accessibility/references/violations.md +0 -0
- /package/plugins/{devflow-code-review → devflow-frontend-design}/skills/frontend-design/SKILL.md +0 -0
- /package/plugins/{devflow-code-review → devflow-frontend-design}/skills/frontend-design/references/detection.md +0 -0
- /package/plugins/{devflow-code-review → devflow-frontend-design}/skills/frontend-design/references/patterns.md +0 -0
- /package/plugins/{devflow-code-review → devflow-frontend-design}/skills/frontend-design/references/violations.md +0 -0
- /package/plugins/{devflow-code-review → devflow-react}/skills/react/references/violations.md +0 -0
- /package/plugins/{devflow-core-skills → devflow-typescript}/skills/typescript/SKILL.md +0 -0
- /package/plugins/{devflow-core-skills → devflow-typescript}/skills/typescript/references/violations.md +0 -0
|
@@ -1,1331 +0,0 @@
|
|
|
1
|
-
# React Correct Patterns
|
|
2
|
-
|
|
3
|
-
Extended correct patterns for React development. Reference from main SKILL.md.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## Vercel Performance Patterns
|
|
8
|
-
|
|
9
|
-
### Async Parallelization
|
|
10
|
-
|
|
11
|
-
```tsx
|
|
12
|
-
// CORRECT: Parallel independent fetches with named destructuring
|
|
13
|
-
async function loadUserDashboard(userId: string) {
|
|
14
|
-
const [
|
|
15
|
-
{ data: user },
|
|
16
|
-
{ data: orders },
|
|
17
|
-
{ data: notifications },
|
|
18
|
-
{ data: preferences },
|
|
19
|
-
] = await Promise.all([
|
|
20
|
-
fetchUser(userId),
|
|
21
|
-
fetchOrders(userId),
|
|
22
|
-
fetchNotifications(userId),
|
|
23
|
-
fetchPreferences(userId),
|
|
24
|
-
]);
|
|
25
|
-
|
|
26
|
-
return {
|
|
27
|
-
user,
|
|
28
|
-
orders,
|
|
29
|
-
notifications,
|
|
30
|
-
preferences,
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// CORRECT: Partial parallelization when some deps exist
|
|
35
|
-
async function loadOrderDetails(orderId: string) {
|
|
36
|
-
// First: fetch order (needed for customer ID)
|
|
37
|
-
const order = await fetchOrder(orderId);
|
|
38
|
-
|
|
39
|
-
// Then: parallel fetch using order data
|
|
40
|
-
const [customer, products, shipping] = await Promise.all([
|
|
41
|
-
fetchCustomer(order.customerId),
|
|
42
|
-
fetchProducts(order.productIds),
|
|
43
|
-
fetchShippingStatus(order.trackingId),
|
|
44
|
-
]);
|
|
45
|
-
|
|
46
|
-
return { order, customer, products, shipping };
|
|
47
|
-
}
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
### Bundle Optimization
|
|
51
|
-
|
|
52
|
-
```tsx
|
|
53
|
-
// CORRECT: Direct component imports (tree-shakable)
|
|
54
|
-
import { Button } from '@/components/ui/Button';
|
|
55
|
-
import { Input } from '@/components/ui/Input';
|
|
56
|
-
import { Select } from '@/components/ui/Select';
|
|
57
|
-
|
|
58
|
-
// CORRECT: Direct icon imports
|
|
59
|
-
import { ChevronDown } from 'lucide-react/dist/esm/icons/chevron-down';
|
|
60
|
-
import { Search } from 'lucide-react/dist/esm/icons/search';
|
|
61
|
-
|
|
62
|
-
// CORRECT: Route-based code splitting
|
|
63
|
-
const Dashboard = lazy(() => import('./pages/Dashboard'));
|
|
64
|
-
const Settings = lazy(() => import('./pages/Settings'));
|
|
65
|
-
const Analytics = lazy(() => import('./pages/Analytics'));
|
|
66
|
-
|
|
67
|
-
function App() {
|
|
68
|
-
return (
|
|
69
|
-
<Suspense fallback={<PageSkeleton />}>
|
|
70
|
-
<Routes>
|
|
71
|
-
<Route path="/dashboard" element={<Dashboard />} />
|
|
72
|
-
<Route path="/settings" element={<Settings />} />
|
|
73
|
-
<Route path="/analytics" element={<Analytics />} />
|
|
74
|
-
</Routes>
|
|
75
|
-
</Suspense>
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// CORRECT: Conditional lazy loading
|
|
80
|
-
function Editor({ showPreview }: { showPreview: boolean }) {
|
|
81
|
-
const [Preview, setPreview] = useState<ComponentType | null>(null);
|
|
82
|
-
|
|
83
|
-
useEffect(() => {
|
|
84
|
-
if (showPreview && !Preview) {
|
|
85
|
-
import('./Preview').then((mod) => setPreview(() => mod.default));
|
|
86
|
-
}
|
|
87
|
-
}, [showPreview, Preview]);
|
|
88
|
-
|
|
89
|
-
return (
|
|
90
|
-
<div>
|
|
91
|
-
<EditorPane />
|
|
92
|
-
{Preview && <Preview />}
|
|
93
|
-
</div>
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
### Re-render Prevention
|
|
99
|
-
|
|
100
|
-
```tsx
|
|
101
|
-
// CORRECT: Extract primitives from objects for deps
|
|
102
|
-
function UserOrders({ user }: { user: User }) {
|
|
103
|
-
const userId = user.id;
|
|
104
|
-
const isActive = user.status === 'active';
|
|
105
|
-
|
|
106
|
-
useEffect(() => {
|
|
107
|
-
if (isActive) {
|
|
108
|
-
fetchOrders(userId);
|
|
109
|
-
}
|
|
110
|
-
}, [userId, isActive]); // primitives = stable deps
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// CORRECT: Stable callback references
|
|
114
|
-
function DataTable({ items, onSelect }: Props) {
|
|
115
|
-
const handleRowClick = useCallback((item: Item) => {
|
|
116
|
-
onSelect(item.id);
|
|
117
|
-
}, [onSelect]);
|
|
118
|
-
|
|
119
|
-
return (
|
|
120
|
-
<table>
|
|
121
|
-
{items.map((item) => (
|
|
122
|
-
<MemoizedRow
|
|
123
|
-
key={item.id}
|
|
124
|
-
item={item}
|
|
125
|
-
onClick={handleRowClick}
|
|
126
|
-
/>
|
|
127
|
-
))}
|
|
128
|
-
</table>
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const MemoizedRow = memo(function Row({
|
|
133
|
-
item,
|
|
134
|
-
onClick,
|
|
135
|
-
}: {
|
|
136
|
-
item: Item;
|
|
137
|
-
onClick: (item: Item) => void;
|
|
138
|
-
}) {
|
|
139
|
-
return (
|
|
140
|
-
<tr onClick={() => onClick(item)}>
|
|
141
|
-
<td>{item.name}</td>
|
|
142
|
-
</tr>
|
|
143
|
-
);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
// CORRECT: Memoize derived collections
|
|
147
|
-
function FilteredList({ items, filter }: Props) {
|
|
148
|
-
const filteredItems = useMemo(
|
|
149
|
-
() => items.filter((item) => item.name.includes(filter)),
|
|
150
|
-
[items, filter]
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
// Also memoize the Set for O(1) lookups
|
|
154
|
-
const itemIds = useMemo(
|
|
155
|
-
() => new Set(filteredItems.map((i) => i.id)),
|
|
156
|
-
[filteredItems]
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
const isVisible = useCallback(
|
|
160
|
-
(id: string) => itemIds.has(id),
|
|
161
|
-
[itemIds]
|
|
162
|
-
);
|
|
163
|
-
|
|
164
|
-
return <List items={filteredItems} isVisible={isVisible} />;
|
|
165
|
-
}
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
### Image Optimization
|
|
169
|
-
|
|
170
|
-
```tsx
|
|
171
|
-
// CORRECT: Fully optimized image component
|
|
172
|
-
interface OptimizedImageProps {
|
|
173
|
-
src: string;
|
|
174
|
-
alt: string;
|
|
175
|
-
width: number;
|
|
176
|
-
height: number;
|
|
177
|
-
priority?: boolean;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function OptimizedImage({
|
|
181
|
-
src,
|
|
182
|
-
alt,
|
|
183
|
-
width,
|
|
184
|
-
height,
|
|
185
|
-
priority = false,
|
|
186
|
-
}: OptimizedImageProps) {
|
|
187
|
-
return (
|
|
188
|
-
<img
|
|
189
|
-
src={src}
|
|
190
|
-
alt={alt}
|
|
191
|
-
width={width}
|
|
192
|
-
height={height}
|
|
193
|
-
loading={priority ? 'eager' : 'lazy'}
|
|
194
|
-
decoding="async"
|
|
195
|
-
style={{
|
|
196
|
-
aspectRatio: `${width}/${height}`,
|
|
197
|
-
objectFit: 'cover',
|
|
198
|
-
}}
|
|
199
|
-
/>
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// CORRECT: Responsive images with srcset
|
|
204
|
-
function ResponsiveImage({ src, alt }: { src: string; alt: string }) {
|
|
205
|
-
return (
|
|
206
|
-
<img
|
|
207
|
-
src={`${src}?w=800`}
|
|
208
|
-
srcSet={`
|
|
209
|
-
${src}?w=400 400w,
|
|
210
|
-
${src}?w=800 800w,
|
|
211
|
-
${src}?w=1200 1200w
|
|
212
|
-
`}
|
|
213
|
-
sizes="(max-width: 400px) 400px, (max-width: 800px) 800px, 1200px"
|
|
214
|
-
alt={alt}
|
|
215
|
-
loading="lazy"
|
|
216
|
-
decoding="async"
|
|
217
|
-
/>
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// CORRECT: Image with blur placeholder
|
|
222
|
-
function ImageWithPlaceholder({ src, alt, width, height }: Props) {
|
|
223
|
-
const [loaded, setLoaded] = useState(false);
|
|
224
|
-
|
|
225
|
-
return (
|
|
226
|
-
<div
|
|
227
|
-
style={{
|
|
228
|
-
position: 'relative',
|
|
229
|
-
width,
|
|
230
|
-
height,
|
|
231
|
-
backgroundColor: '#f0f0f0',
|
|
232
|
-
}}
|
|
233
|
-
>
|
|
234
|
-
<img
|
|
235
|
-
src={src}
|
|
236
|
-
alt={alt}
|
|
237
|
-
width={width}
|
|
238
|
-
height={height}
|
|
239
|
-
loading="lazy"
|
|
240
|
-
decoding="async"
|
|
241
|
-
onLoad={() => setLoaded(true)}
|
|
242
|
-
style={{
|
|
243
|
-
opacity: loaded ? 1 : 0,
|
|
244
|
-
transition: 'opacity 0.3s',
|
|
245
|
-
}}
|
|
246
|
-
/>
|
|
247
|
-
</div>
|
|
248
|
-
);
|
|
249
|
-
}
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
### Data Structure Performance
|
|
253
|
-
|
|
254
|
-
```tsx
|
|
255
|
-
// CORRECT: Map for complex key-value operations
|
|
256
|
-
function useUsersMap(users: User[]) {
|
|
257
|
-
return useMemo(() => {
|
|
258
|
-
const byId = new Map<string, User>();
|
|
259
|
-
const byEmail = new Map<string, User>();
|
|
260
|
-
|
|
261
|
-
for (const user of users) {
|
|
262
|
-
byId.set(user.id, user);
|
|
263
|
-
byEmail.set(user.email.toLowerCase(), user);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
return {
|
|
267
|
-
getById: (id: string) => byId.get(id),
|
|
268
|
-
getByEmail: (email: string) => byEmail.get(email.toLowerCase()),
|
|
269
|
-
has: (id: string) => byId.has(id),
|
|
270
|
-
};
|
|
271
|
-
}, [users]);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// CORRECT: Set for selection state
|
|
275
|
-
function useSelection<T extends string>() {
|
|
276
|
-
const [selected, setSelected] = useState<Set<T>>(() => new Set());
|
|
277
|
-
|
|
278
|
-
const toggle = useCallback((id: T) => {
|
|
279
|
-
setSelected((prev) => {
|
|
280
|
-
const next = new Set(prev);
|
|
281
|
-
if (next.has(id)) {
|
|
282
|
-
next.delete(id);
|
|
283
|
-
} else {
|
|
284
|
-
next.add(id);
|
|
285
|
-
}
|
|
286
|
-
return next;
|
|
287
|
-
});
|
|
288
|
-
}, []);
|
|
289
|
-
|
|
290
|
-
const isSelected = useCallback(
|
|
291
|
-
(id: T) => selected.has(id),
|
|
292
|
-
[selected]
|
|
293
|
-
);
|
|
294
|
-
|
|
295
|
-
const selectAll = useCallback((ids: T[]) => {
|
|
296
|
-
setSelected(new Set(ids));
|
|
297
|
-
}, []);
|
|
298
|
-
|
|
299
|
-
const clear = useCallback(() => {
|
|
300
|
-
setSelected(new Set());
|
|
301
|
-
}, []);
|
|
302
|
-
|
|
303
|
-
return { selected, toggle, isSelected, selectAll, clear };
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// CORRECT: Efficient list filtering with index
|
|
307
|
-
function useFilteredList<T extends { id: string }>(
|
|
308
|
-
items: T[],
|
|
309
|
-
filterFn: (item: T) => boolean
|
|
310
|
-
) {
|
|
311
|
-
return useMemo(() => {
|
|
312
|
-
const filtered = items.filter(filterFn);
|
|
313
|
-
const indexById = new Map(filtered.map((item, i) => [item.id, i]));
|
|
314
|
-
|
|
315
|
-
return {
|
|
316
|
-
items: filtered,
|
|
317
|
-
count: filtered.length,
|
|
318
|
-
indexOf: (id: string) => indexById.get(id) ?? -1,
|
|
319
|
-
includes: (id: string) => indexById.has(id),
|
|
320
|
-
};
|
|
321
|
-
}, [items, filterFn]);
|
|
322
|
-
}
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
---
|
|
326
|
-
|
|
327
|
-
## Component Patterns
|
|
328
|
-
|
|
329
|
-
### Composition with Compound Components
|
|
330
|
-
|
|
331
|
-
```tsx
|
|
332
|
-
// CORRECT: Flexible composition through children
|
|
333
|
-
function Card({ children }: { children: React.ReactNode }) {
|
|
334
|
-
return <div className="card">{children}</div>;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
Card.Header = function CardHeader({ children }: { children: React.ReactNode }) {
|
|
338
|
-
return <div className="card-header">{children}</div>;
|
|
339
|
-
};
|
|
340
|
-
|
|
341
|
-
Card.Body = function CardBody({ children }: { children: React.ReactNode }) {
|
|
342
|
-
return <div className="card-body">{children}</div>;
|
|
343
|
-
};
|
|
344
|
-
|
|
345
|
-
Card.Footer = function CardFooter({ children }: { children: React.ReactNode }) {
|
|
346
|
-
return <div className="card-footer">{children}</div>;
|
|
347
|
-
};
|
|
348
|
-
|
|
349
|
-
// Usage - flexible, composable
|
|
350
|
-
<Card>
|
|
351
|
-
<Card.Header>
|
|
352
|
-
<h2>Title</h2>
|
|
353
|
-
<CloseButton onClick={onClose} />
|
|
354
|
-
</Card.Header>
|
|
355
|
-
<Card.Body>
|
|
356
|
-
<p>Content goes here</p>
|
|
357
|
-
</Card.Body>
|
|
358
|
-
<Card.Footer>
|
|
359
|
-
<Button>Save</Button>
|
|
360
|
-
</Card.Footer>
|
|
361
|
-
</Card>
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
### Render Props Pattern
|
|
365
|
-
|
|
366
|
-
```tsx
|
|
367
|
-
// CORRECT: Share logic, customize rendering
|
|
368
|
-
interface DataFetcherProps<T> {
|
|
369
|
-
url: string;
|
|
370
|
-
children: (data: T | null, loading: boolean, error: Error | null) => React.ReactNode;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function DataFetcher<T>({ url, children }: DataFetcherProps<T>) {
|
|
374
|
-
const [data, setData] = useState<T | null>(null);
|
|
375
|
-
const [loading, setLoading] = useState(true);
|
|
376
|
-
const [error, setError] = useState<Error | null>(null);
|
|
377
|
-
|
|
378
|
-
useEffect(() => {
|
|
379
|
-
fetch(url)
|
|
380
|
-
.then((res) => res.json())
|
|
381
|
-
.then(setData)
|
|
382
|
-
.catch(setError)
|
|
383
|
-
.finally(() => setLoading(false));
|
|
384
|
-
}, [url]);
|
|
385
|
-
|
|
386
|
-
return <>{children(data, loading, error)}</>;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// Usage
|
|
390
|
-
<DataFetcher<User> url="/api/user">
|
|
391
|
-
{(user, loading, error) => {
|
|
392
|
-
if (loading) return <Spinner />;
|
|
393
|
-
if (error) return <ErrorMessage error={error} />;
|
|
394
|
-
if (!user) return <NotFound />;
|
|
395
|
-
return <UserProfile user={user} />;
|
|
396
|
-
}}
|
|
397
|
-
</DataFetcher>
|
|
398
|
-
```
|
|
399
|
-
|
|
400
|
-
### Context for Shared State
|
|
401
|
-
|
|
402
|
-
```tsx
|
|
403
|
-
// CORRECT: Avoid prop drilling with context
|
|
404
|
-
interface AuthContextValue {
|
|
405
|
-
user: User | null;
|
|
406
|
-
login: (credentials: Credentials) => Promise<void>;
|
|
407
|
-
logout: () => void;
|
|
408
|
-
isLoading: boolean;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
const AuthContext = createContext<AuthContextValue | null>(null);
|
|
412
|
-
|
|
413
|
-
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|
414
|
-
const [user, setUser] = useState<User | null>(null);
|
|
415
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
416
|
-
|
|
417
|
-
useEffect(() => {
|
|
418
|
-
checkSession().then(setUser).finally(() => setIsLoading(false));
|
|
419
|
-
}, []);
|
|
420
|
-
|
|
421
|
-
const login = async (credentials: Credentials) => {
|
|
422
|
-
const user = await authApi.login(credentials);
|
|
423
|
-
setUser(user);
|
|
424
|
-
};
|
|
425
|
-
|
|
426
|
-
const logout = () => {
|
|
427
|
-
authApi.logout();
|
|
428
|
-
setUser(null);
|
|
429
|
-
};
|
|
430
|
-
|
|
431
|
-
return (
|
|
432
|
-
<AuthContext.Provider value={{ user, login, logout, isLoading }}>
|
|
433
|
-
{children}
|
|
434
|
-
</AuthContext.Provider>
|
|
435
|
-
);
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
export function useAuth(): AuthContextValue {
|
|
439
|
-
const context = useContext(AuthContext);
|
|
440
|
-
if (!context) {
|
|
441
|
-
throw new Error('useAuth must be used within AuthProvider');
|
|
442
|
-
}
|
|
443
|
-
return context;
|
|
444
|
-
}
|
|
445
|
-
```
|
|
446
|
-
|
|
447
|
-
### Virtualization for Long Lists
|
|
448
|
-
|
|
449
|
-
```tsx
|
|
450
|
-
// CORRECT: Only render visible items
|
|
451
|
-
import { FixedSizeList } from 'react-window';
|
|
452
|
-
|
|
453
|
-
function VirtualizedList({ items }: { items: Item[] }) {
|
|
454
|
-
return (
|
|
455
|
-
<FixedSizeList
|
|
456
|
-
height={400}
|
|
457
|
-
width="100%"
|
|
458
|
-
itemCount={items.length}
|
|
459
|
-
itemSize={50}
|
|
460
|
-
>
|
|
461
|
-
{({ index, style }) => (
|
|
462
|
-
<div style={style}>
|
|
463
|
-
<ItemRow item={items[index]} />
|
|
464
|
-
</div>
|
|
465
|
-
)}
|
|
466
|
-
</FixedSizeList>
|
|
467
|
-
);
|
|
468
|
-
}
|
|
469
|
-
```
|
|
470
|
-
|
|
471
|
-
### Lazy Loading Components
|
|
472
|
-
|
|
473
|
-
```tsx
|
|
474
|
-
// CORRECT: Load heavy components on demand
|
|
475
|
-
const HeavyComponent = lazy(() => import('./HeavyComponent'));
|
|
476
|
-
|
|
477
|
-
function App() {
|
|
478
|
-
return (
|
|
479
|
-
<Suspense fallback={<Spinner />}>
|
|
480
|
-
<HeavyComponent />
|
|
481
|
-
</Suspense>
|
|
482
|
-
);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
// Lazy load on interaction
|
|
486
|
-
function Dashboard() {
|
|
487
|
-
const [showChart, setShowChart] = useState(false);
|
|
488
|
-
|
|
489
|
-
return (
|
|
490
|
-
<div>
|
|
491
|
-
<button onClick={() => setShowChart(true)}>Show Chart</button>
|
|
492
|
-
{showChart && (
|
|
493
|
-
<Suspense fallback={<ChartSkeleton />}>
|
|
494
|
-
<LazyChart />
|
|
495
|
-
</Suspense>
|
|
496
|
-
)}
|
|
497
|
-
</div>
|
|
498
|
-
);
|
|
499
|
-
}
|
|
500
|
-
```
|
|
501
|
-
|
|
502
|
-
---
|
|
503
|
-
|
|
504
|
-
## Hooks Patterns
|
|
505
|
-
|
|
506
|
-
### Data Fetching Hook
|
|
507
|
-
|
|
508
|
-
```tsx
|
|
509
|
-
// CORRECT: Reusable data fetching logic
|
|
510
|
-
interface UseQueryResult<T> {
|
|
511
|
-
data: T | undefined;
|
|
512
|
-
isLoading: boolean;
|
|
513
|
-
error: Error | undefined;
|
|
514
|
-
refetch: () => void;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
function useQuery<T>(url: string): UseQueryResult<T> {
|
|
518
|
-
const [data, setData] = useState<T>();
|
|
519
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
520
|
-
const [error, setError] = useState<Error>();
|
|
521
|
-
|
|
522
|
-
const fetchData = useCallback(async () => {
|
|
523
|
-
setIsLoading(true);
|
|
524
|
-
setError(undefined);
|
|
525
|
-
try {
|
|
526
|
-
const response = await fetch(url);
|
|
527
|
-
if (!response.ok) throw new Error('Failed to fetch');
|
|
528
|
-
const json = await response.json();
|
|
529
|
-
setData(json);
|
|
530
|
-
} catch (e) {
|
|
531
|
-
setError(e as Error);
|
|
532
|
-
} finally {
|
|
533
|
-
setIsLoading(false);
|
|
534
|
-
}
|
|
535
|
-
}, [url]);
|
|
536
|
-
|
|
537
|
-
useEffect(() => {
|
|
538
|
-
fetchData();
|
|
539
|
-
}, [fetchData]);
|
|
540
|
-
|
|
541
|
-
return { data, isLoading, error, refetch: fetchData };
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
// Usage
|
|
545
|
-
function UserList() {
|
|
546
|
-
const { data: users, isLoading, error, refetch } = useQuery<User[]>('/api/users');
|
|
547
|
-
|
|
548
|
-
if (isLoading) return <Spinner />;
|
|
549
|
-
if (error) return <Error message={error.message} onRetry={refetch} />;
|
|
550
|
-
|
|
551
|
-
return <ul>{users?.map(user => <li key={user.id}>{user.name}</li>)}</ul>;
|
|
552
|
-
}
|
|
553
|
-
```
|
|
554
|
-
|
|
555
|
-
### Debounced Value Hook
|
|
556
|
-
|
|
557
|
-
```tsx
|
|
558
|
-
// CORRECT: Debounce rapid value changes
|
|
559
|
-
function useDebouncedValue<T>(value: T, delayMs: number): T {
|
|
560
|
-
const [debouncedValue, setDebouncedValue] = useState(value);
|
|
561
|
-
|
|
562
|
-
useEffect(() => {
|
|
563
|
-
const timer = setTimeout(() => {
|
|
564
|
-
setDebouncedValue(value);
|
|
565
|
-
}, delayMs);
|
|
566
|
-
|
|
567
|
-
return () => clearTimeout(timer);
|
|
568
|
-
}, [value, delayMs]);
|
|
569
|
-
|
|
570
|
-
return debouncedValue;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
// Usage
|
|
574
|
-
function SearchInput() {
|
|
575
|
-
const [query, setQuery] = useState('');
|
|
576
|
-
const debouncedQuery = useDebouncedValue(query, 300);
|
|
577
|
-
|
|
578
|
-
useEffect(() => {
|
|
579
|
-
if (debouncedQuery) {
|
|
580
|
-
searchApi(debouncedQuery);
|
|
581
|
-
}
|
|
582
|
-
}, [debouncedQuery]);
|
|
583
|
-
|
|
584
|
-
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
|
|
585
|
-
}
|
|
586
|
-
```
|
|
587
|
-
|
|
588
|
-
### Previous Value Hook
|
|
589
|
-
|
|
590
|
-
```tsx
|
|
591
|
-
// CORRECT: Track previous value for comparisons
|
|
592
|
-
function usePrevious<T>(value: T): T | undefined {
|
|
593
|
-
const ref = useRef<T>();
|
|
594
|
-
|
|
595
|
-
useEffect(() => {
|
|
596
|
-
ref.current = value;
|
|
597
|
-
}, [value]);
|
|
598
|
-
|
|
599
|
-
return ref.current;
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
// Usage
|
|
603
|
-
function Counter({ count }: { count: number }) {
|
|
604
|
-
const prevCount = usePrevious(count);
|
|
605
|
-
|
|
606
|
-
return (
|
|
607
|
-
<div>
|
|
608
|
-
Current: {count}, Previous: {prevCount ?? 'N/A'}
|
|
609
|
-
</div>
|
|
610
|
-
);
|
|
611
|
-
}
|
|
612
|
-
```
|
|
613
|
-
|
|
614
|
-
### Toggle Hook
|
|
615
|
-
|
|
616
|
-
```tsx
|
|
617
|
-
// CORRECT: Simple boolean state toggle
|
|
618
|
-
function useToggle(initialValue = false): [boolean, () => void] {
|
|
619
|
-
const [value, setValue] = useState(initialValue);
|
|
620
|
-
const toggle = useCallback(() => setValue(v => !v), []);
|
|
621
|
-
return [value, toggle];
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
// Usage
|
|
625
|
-
function Modal() {
|
|
626
|
-
const [isOpen, toggleOpen] = useToggle();
|
|
627
|
-
|
|
628
|
-
return (
|
|
629
|
-
<>
|
|
630
|
-
<button onClick={toggleOpen}>Toggle Modal</button>
|
|
631
|
-
{isOpen && <ModalContent onClose={toggleOpen} />}
|
|
632
|
-
</>
|
|
633
|
-
);
|
|
634
|
-
}
|
|
635
|
-
```
|
|
636
|
-
|
|
637
|
-
### Media Query Hook
|
|
638
|
-
|
|
639
|
-
```tsx
|
|
640
|
-
// CORRECT: Responsive component logic
|
|
641
|
-
function useMediaQuery(query: string): boolean {
|
|
642
|
-
const [matches, setMatches] = useState(
|
|
643
|
-
() => window.matchMedia(query).matches
|
|
644
|
-
);
|
|
645
|
-
|
|
646
|
-
useEffect(() => {
|
|
647
|
-
const mediaQuery = window.matchMedia(query);
|
|
648
|
-
const handler = (event: MediaQueryListEvent) => setMatches(event.matches);
|
|
649
|
-
|
|
650
|
-
mediaQuery.addEventListener('change', handler);
|
|
651
|
-
return () => mediaQuery.removeEventListener('change', handler);
|
|
652
|
-
}, [query]);
|
|
653
|
-
|
|
654
|
-
return matches;
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
// Usage
|
|
658
|
-
function ResponsiveComponent() {
|
|
659
|
-
const isMobile = useMediaQuery('(max-width: 768px)');
|
|
660
|
-
|
|
661
|
-
return isMobile ? <MobileLayout /> : <DesktopLayout />;
|
|
662
|
-
}
|
|
663
|
-
```
|
|
664
|
-
|
|
665
|
-
### Click Outside Hook
|
|
666
|
-
|
|
667
|
-
```tsx
|
|
668
|
-
// CORRECT: Detect clicks outside element
|
|
669
|
-
function useClickOutside<T extends HTMLElement>(
|
|
670
|
-
callback: () => void
|
|
671
|
-
): React.RefObject<T> {
|
|
672
|
-
const ref = useRef<T>(null);
|
|
673
|
-
|
|
674
|
-
useEffect(() => {
|
|
675
|
-
const handleClick = (event: MouseEvent) => {
|
|
676
|
-
if (ref.current && !ref.current.contains(event.target as Node)) {
|
|
677
|
-
callback();
|
|
678
|
-
}
|
|
679
|
-
};
|
|
680
|
-
|
|
681
|
-
document.addEventListener('mousedown', handleClick);
|
|
682
|
-
return () => document.removeEventListener('mousedown', handleClick);
|
|
683
|
-
}, [callback]);
|
|
684
|
-
|
|
685
|
-
return ref;
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
// Usage
|
|
689
|
-
function Dropdown() {
|
|
690
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
691
|
-
const dropdownRef = useClickOutside<HTMLDivElement>(() => setIsOpen(false));
|
|
692
|
-
|
|
693
|
-
return (
|
|
694
|
-
<div ref={dropdownRef}>
|
|
695
|
-
<button onClick={() => setIsOpen(true)}>Open</button>
|
|
696
|
-
{isOpen && <DropdownMenu />}
|
|
697
|
-
</div>
|
|
698
|
-
);
|
|
699
|
-
}
|
|
700
|
-
```
|
|
701
|
-
|
|
702
|
-
### Reducer for Complex State
|
|
703
|
-
|
|
704
|
-
```tsx
|
|
705
|
-
// CORRECT: Manage complex state transitions
|
|
706
|
-
interface FormState {
|
|
707
|
-
values: Record<string, string>;
|
|
708
|
-
errors: Record<string, string>;
|
|
709
|
-
touched: Record<string, boolean>;
|
|
710
|
-
isSubmitting: boolean;
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
type FormAction =
|
|
714
|
-
| { type: 'SET_VALUE'; field: string; value: string }
|
|
715
|
-
| { type: 'SET_ERROR'; field: string; error: string }
|
|
716
|
-
| { type: 'SET_TOUCHED'; field: string }
|
|
717
|
-
| { type: 'SUBMIT_START' }
|
|
718
|
-
| { type: 'SUBMIT_END' }
|
|
719
|
-
| { type: 'RESET' };
|
|
720
|
-
|
|
721
|
-
function formReducer(state: FormState, action: FormAction): FormState {
|
|
722
|
-
switch (action.type) {
|
|
723
|
-
case 'SET_VALUE':
|
|
724
|
-
return {
|
|
725
|
-
...state,
|
|
726
|
-
values: { ...state.values, [action.field]: action.value },
|
|
727
|
-
errors: { ...state.errors, [action.field]: '' },
|
|
728
|
-
};
|
|
729
|
-
case 'SET_ERROR':
|
|
730
|
-
return {
|
|
731
|
-
...state,
|
|
732
|
-
errors: { ...state.errors, [action.field]: action.error },
|
|
733
|
-
};
|
|
734
|
-
case 'SET_TOUCHED':
|
|
735
|
-
return {
|
|
736
|
-
...state,
|
|
737
|
-
touched: { ...state.touched, [action.field]: true },
|
|
738
|
-
};
|
|
739
|
-
case 'SUBMIT_START':
|
|
740
|
-
return { ...state, isSubmitting: true };
|
|
741
|
-
case 'SUBMIT_END':
|
|
742
|
-
return { ...state, isSubmitting: false };
|
|
743
|
-
case 'RESET':
|
|
744
|
-
return initialFormState;
|
|
745
|
-
default:
|
|
746
|
-
return state;
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
// Usage
|
|
751
|
-
function useForm() {
|
|
752
|
-
const [state, dispatch] = useReducer(formReducer, initialFormState);
|
|
753
|
-
|
|
754
|
-
const setValue = (field: string, value: string) => {
|
|
755
|
-
dispatch({ type: 'SET_VALUE', field, value });
|
|
756
|
-
};
|
|
757
|
-
|
|
758
|
-
const setTouched = (field: string) => {
|
|
759
|
-
dispatch({ type: 'SET_TOUCHED', field });
|
|
760
|
-
};
|
|
761
|
-
|
|
762
|
-
return { ...state, setValue, setTouched };
|
|
763
|
-
}
|
|
764
|
-
```
|
|
765
|
-
|
|
766
|
-
---
|
|
767
|
-
|
|
768
|
-
## Forms Patterns
|
|
769
|
-
|
|
770
|
-
### Controlled Form with Validation
|
|
771
|
-
|
|
772
|
-
```tsx
|
|
773
|
-
// CORRECT: Full validation, accessibility, error display
|
|
774
|
-
interface FormData {
|
|
775
|
-
email: string;
|
|
776
|
-
password: string;
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
function LoginForm({ onSubmit }: { onSubmit: (data: FormData) => void }) {
|
|
780
|
-
const [formData, setFormData] = useState<FormData>({
|
|
781
|
-
email: '',
|
|
782
|
-
password: '',
|
|
783
|
-
});
|
|
784
|
-
const [errors, setErrors] = useState<Partial<FormData>>({});
|
|
785
|
-
|
|
786
|
-
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
787
|
-
const { name, value } = e.target;
|
|
788
|
-
setFormData((prev) => ({ ...prev, [name]: value }));
|
|
789
|
-
setErrors((prev) => ({ ...prev, [name]: '' }));
|
|
790
|
-
};
|
|
791
|
-
|
|
792
|
-
const validate = (): boolean => {
|
|
793
|
-
const newErrors: Partial<FormData> = {};
|
|
794
|
-
if (!formData.email) newErrors.email = 'Email required';
|
|
795
|
-
if (!formData.password) newErrors.password = 'Password required';
|
|
796
|
-
setErrors(newErrors);
|
|
797
|
-
return Object.keys(newErrors).length === 0;
|
|
798
|
-
};
|
|
799
|
-
|
|
800
|
-
const handleSubmit = (e: React.FormEvent) => {
|
|
801
|
-
e.preventDefault();
|
|
802
|
-
if (validate()) {
|
|
803
|
-
onSubmit(formData);
|
|
804
|
-
}
|
|
805
|
-
};
|
|
806
|
-
|
|
807
|
-
return (
|
|
808
|
-
<form onSubmit={handleSubmit}>
|
|
809
|
-
<div>
|
|
810
|
-
<input
|
|
811
|
-
name="email"
|
|
812
|
-
type="email"
|
|
813
|
-
value={formData.email}
|
|
814
|
-
onChange={handleChange}
|
|
815
|
-
aria-invalid={!!errors.email}
|
|
816
|
-
/>
|
|
817
|
-
{errors.email && <span role="alert">{errors.email}</span>}
|
|
818
|
-
</div>
|
|
819
|
-
<div>
|
|
820
|
-
<input
|
|
821
|
-
name="password"
|
|
822
|
-
type="password"
|
|
823
|
-
value={formData.password}
|
|
824
|
-
onChange={handleChange}
|
|
825
|
-
aria-invalid={!!errors.password}
|
|
826
|
-
/>
|
|
827
|
-
{errors.password && <span role="alert">{errors.password}</span>}
|
|
828
|
-
</div>
|
|
829
|
-
<button type="submit">Login</button>
|
|
830
|
-
</form>
|
|
831
|
-
);
|
|
832
|
-
}
|
|
833
|
-
```
|
|
834
|
-
|
|
835
|
-
### Form with Validation Hook
|
|
836
|
-
|
|
837
|
-
```tsx
|
|
838
|
-
// CORRECT: Reusable form handling logic
|
|
839
|
-
interface UseFormOptions<T> {
|
|
840
|
-
initialValues: T;
|
|
841
|
-
validate: (values: T) => Partial<Record<keyof T, string>>;
|
|
842
|
-
onSubmit: (values: T) => void | Promise<void>;
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
function useForm<T extends Record<string, unknown>>({
|
|
846
|
-
initialValues,
|
|
847
|
-
validate,
|
|
848
|
-
onSubmit,
|
|
849
|
-
}: UseFormOptions<T>) {
|
|
850
|
-
const [values, setValues] = useState(initialValues);
|
|
851
|
-
const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
|
|
852
|
-
const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({});
|
|
853
|
-
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
854
|
-
|
|
855
|
-
const handleChange = (name: keyof T) => (
|
|
856
|
-
e: React.ChangeEvent<HTMLInputElement>
|
|
857
|
-
) => {
|
|
858
|
-
setValues((prev) => ({ ...prev, [name]: e.target.value }));
|
|
859
|
-
if (errors[name]) {
|
|
860
|
-
setErrors((prev) => ({ ...prev, [name]: undefined }));
|
|
861
|
-
}
|
|
862
|
-
};
|
|
863
|
-
|
|
864
|
-
const handleBlur = (name: keyof T) => () => {
|
|
865
|
-
setTouched((prev) => ({ ...prev, [name]: true }));
|
|
866
|
-
const fieldErrors = validate(values);
|
|
867
|
-
if (fieldErrors[name]) {
|
|
868
|
-
setErrors((prev) => ({ ...prev, [name]: fieldErrors[name] }));
|
|
869
|
-
}
|
|
870
|
-
};
|
|
871
|
-
|
|
872
|
-
const handleSubmit = async (e: React.FormEvent) => {
|
|
873
|
-
e.preventDefault();
|
|
874
|
-
const validationErrors = validate(values);
|
|
875
|
-
setErrors(validationErrors);
|
|
876
|
-
setTouched(
|
|
877
|
-
Object.keys(values).reduce(
|
|
878
|
-
(acc, key) => ({ ...acc, [key]: true }),
|
|
879
|
-
{} as Record<keyof T, boolean>
|
|
880
|
-
)
|
|
881
|
-
);
|
|
882
|
-
|
|
883
|
-
if (Object.keys(validationErrors).length === 0) {
|
|
884
|
-
setIsSubmitting(true);
|
|
885
|
-
try {
|
|
886
|
-
await onSubmit(values);
|
|
887
|
-
} finally {
|
|
888
|
-
setIsSubmitting(false);
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
};
|
|
892
|
-
|
|
893
|
-
const reset = () => {
|
|
894
|
-
setValues(initialValues);
|
|
895
|
-
setErrors({});
|
|
896
|
-
setTouched({});
|
|
897
|
-
};
|
|
898
|
-
|
|
899
|
-
return {
|
|
900
|
-
values,
|
|
901
|
-
errors,
|
|
902
|
-
touched,
|
|
903
|
-
isSubmitting,
|
|
904
|
-
handleChange,
|
|
905
|
-
handleBlur,
|
|
906
|
-
handleSubmit,
|
|
907
|
-
reset,
|
|
908
|
-
setValues,
|
|
909
|
-
};
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
// Usage
|
|
913
|
-
function SignupForm() {
|
|
914
|
-
const form = useForm({
|
|
915
|
-
initialValues: { name: '', email: '', password: '' },
|
|
916
|
-
validate: (values) => {
|
|
917
|
-
const errors: Partial<typeof values> = {};
|
|
918
|
-
if (!values.name) errors.name = 'Name required';
|
|
919
|
-
if (!values.email) errors.email = 'Email required';
|
|
920
|
-
if (values.password.length < 8) errors.password = 'Min 8 characters';
|
|
921
|
-
return errors;
|
|
922
|
-
},
|
|
923
|
-
onSubmit: async (values) => {
|
|
924
|
-
await api.signup(values);
|
|
925
|
-
},
|
|
926
|
-
});
|
|
927
|
-
|
|
928
|
-
return (
|
|
929
|
-
<form onSubmit={form.handleSubmit}>
|
|
930
|
-
<input
|
|
931
|
-
value={form.values.name}
|
|
932
|
-
onChange={form.handleChange('name')}
|
|
933
|
-
onBlur={form.handleBlur('name')}
|
|
934
|
-
/>
|
|
935
|
-
{form.touched.name && form.errors.name && (
|
|
936
|
-
<span role="alert">{form.errors.name}</span>
|
|
937
|
-
)}
|
|
938
|
-
{/* ... other fields */}
|
|
939
|
-
<button type="submit" disabled={form.isSubmitting}>
|
|
940
|
-
{form.isSubmitting ? 'Submitting...' : 'Sign Up'}
|
|
941
|
-
</button>
|
|
942
|
-
</form>
|
|
943
|
-
);
|
|
944
|
-
}
|
|
945
|
-
```
|
|
946
|
-
|
|
947
|
-
### Multi-Step Form
|
|
948
|
-
|
|
949
|
-
```tsx
|
|
950
|
-
// CORRECT: Manage state across form steps
|
|
951
|
-
interface StepProps {
|
|
952
|
-
next: () => void;
|
|
953
|
-
prev: () => void;
|
|
954
|
-
data: FormData;
|
|
955
|
-
updateData: (updates: Partial<FormData>) => void;
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
function MultiStepForm() {
|
|
959
|
-
const [step, setStep] = useState(0);
|
|
960
|
-
const [data, setData] = useState<FormData>({
|
|
961
|
-
name: '',
|
|
962
|
-
email: '',
|
|
963
|
-
address: '',
|
|
964
|
-
payment: '',
|
|
965
|
-
});
|
|
966
|
-
|
|
967
|
-
const updateData = (updates: Partial<FormData>) => {
|
|
968
|
-
setData((prev) => ({ ...prev, ...updates }));
|
|
969
|
-
};
|
|
970
|
-
|
|
971
|
-
const next = () => setStep((s) => Math.min(s + 1, steps.length - 1));
|
|
972
|
-
const prev = () => setStep((s) => Math.max(s - 1, 0));
|
|
973
|
-
|
|
974
|
-
const steps = [
|
|
975
|
-
<PersonalInfo {...{ next, prev, data, updateData }} />,
|
|
976
|
-
<AddressInfo {...{ next, prev, data, updateData }} />,
|
|
977
|
-
<PaymentInfo {...{ next, prev, data, updateData }} />,
|
|
978
|
-
<Confirmation {...{ next, prev, data, updateData }} />,
|
|
979
|
-
];
|
|
980
|
-
|
|
981
|
-
return (
|
|
982
|
-
<div>
|
|
983
|
-
<StepIndicator current={step} total={steps.length} />
|
|
984
|
-
{steps[step]}
|
|
985
|
-
</div>
|
|
986
|
-
);
|
|
987
|
-
}
|
|
988
|
-
```
|
|
989
|
-
|
|
990
|
-
### Uncontrolled Form with FormData
|
|
991
|
-
|
|
992
|
-
```tsx
|
|
993
|
-
// CORRECT: Use native FormData API
|
|
994
|
-
function UncontrolledForm({ onSubmit }: { onSubmit: (data: FormData) => void }) {
|
|
995
|
-
const formRef = useRef<HTMLFormElement>(null);
|
|
996
|
-
|
|
997
|
-
const handleSubmit = (e: React.FormEvent) => {
|
|
998
|
-
e.preventDefault();
|
|
999
|
-
if (formRef.current) {
|
|
1000
|
-
const formData = new FormData(formRef.current);
|
|
1001
|
-
onSubmit(formData);
|
|
1002
|
-
}
|
|
1003
|
-
};
|
|
1004
|
-
|
|
1005
|
-
return (
|
|
1006
|
-
<form ref={formRef} onSubmit={handleSubmit}>
|
|
1007
|
-
<input name="email" type="email" required />
|
|
1008
|
-
<input name="password" type="password" required />
|
|
1009
|
-
<button type="submit">Submit</button>
|
|
1010
|
-
</form>
|
|
1011
|
-
);
|
|
1012
|
-
}
|
|
1013
|
-
```
|
|
1014
|
-
|
|
1015
|
-
---
|
|
1016
|
-
|
|
1017
|
-
## Error Handling Patterns
|
|
1018
|
-
|
|
1019
|
-
### Error Boundary Component
|
|
1020
|
-
|
|
1021
|
-
```tsx
|
|
1022
|
-
// CORRECT: Catch render errors
|
|
1023
|
-
interface ErrorBoundaryState {
|
|
1024
|
-
hasError: boolean;
|
|
1025
|
-
error: Error | null;
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
class ErrorBoundary extends Component<
|
|
1029
|
-
{ children: React.ReactNode; fallback?: React.ReactNode },
|
|
1030
|
-
ErrorBoundaryState
|
|
1031
|
-
> {
|
|
1032
|
-
state: ErrorBoundaryState = { hasError: false, error: null };
|
|
1033
|
-
|
|
1034
|
-
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
|
1035
|
-
return { hasError: true, error };
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
componentDidCatch(error: Error, info: ErrorInfo) {
|
|
1039
|
-
console.error('Error caught:', error, info);
|
|
1040
|
-
// Log to error reporting service
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
render() {
|
|
1044
|
-
if (this.state.hasError) {
|
|
1045
|
-
return this.props.fallback ?? <DefaultErrorFallback error={this.state.error} />;
|
|
1046
|
-
}
|
|
1047
|
-
return this.props.children;
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
// Usage
|
|
1052
|
-
<ErrorBoundary fallback={<ErrorPage />}>
|
|
1053
|
-
<App />
|
|
1054
|
-
</ErrorBoundary>
|
|
1055
|
-
```
|
|
1056
|
-
|
|
1057
|
-
### Error Boundary with Reset
|
|
1058
|
-
|
|
1059
|
-
```tsx
|
|
1060
|
-
// CORRECT: Allow recovery from errors
|
|
1061
|
-
interface ErrorBoundaryProps {
|
|
1062
|
-
children: React.ReactNode;
|
|
1063
|
-
fallback?: (error: Error, reset: () => void) => React.ReactNode;
|
|
1064
|
-
onError?: (error: Error, errorInfo: ErrorInfo) => void;
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
class ResettableErrorBoundary extends Component<
|
|
1068
|
-
ErrorBoundaryProps,
|
|
1069
|
-
ErrorBoundaryState
|
|
1070
|
-
> {
|
|
1071
|
-
state: ErrorBoundaryState = { hasError: false, error: null };
|
|
1072
|
-
|
|
1073
|
-
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
|
1074
|
-
return { hasError: true, error };
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
componentDidCatch(error: Error, info: ErrorInfo) {
|
|
1078
|
-
this.props.onError?.(error, info);
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
reset = () => {
|
|
1082
|
-
this.setState({ hasError: false, error: null });
|
|
1083
|
-
};
|
|
1084
|
-
|
|
1085
|
-
render() {
|
|
1086
|
-
if (this.state.hasError && this.state.error) {
|
|
1087
|
-
if (this.props.fallback) {
|
|
1088
|
-
return this.props.fallback(this.state.error, this.reset);
|
|
1089
|
-
}
|
|
1090
|
-
return (
|
|
1091
|
-
<div>
|
|
1092
|
-
<h2>Something went wrong</h2>
|
|
1093
|
-
<button onClick={this.reset}>Try again</button>
|
|
1094
|
-
</div>
|
|
1095
|
-
);
|
|
1096
|
-
}
|
|
1097
|
-
return this.props.children;
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
// Usage
|
|
1102
|
-
<ResettableErrorBoundary
|
|
1103
|
-
fallback={(error, reset) => (
|
|
1104
|
-
<div>
|
|
1105
|
-
<h2>Error: {error.message}</h2>
|
|
1106
|
-
<button onClick={reset}>Retry</button>
|
|
1107
|
-
</div>
|
|
1108
|
-
)}
|
|
1109
|
-
onError={(error) => logErrorToService(error)}
|
|
1110
|
-
>
|
|
1111
|
-
<App />
|
|
1112
|
-
</ResettableErrorBoundary>
|
|
1113
|
-
```
|
|
1114
|
-
|
|
1115
|
-
### Async Error Handling Hook
|
|
1116
|
-
|
|
1117
|
-
```tsx
|
|
1118
|
-
// CORRECT: Handle async operation states
|
|
1119
|
-
interface UseAsyncState<T> {
|
|
1120
|
-
data: T | null;
|
|
1121
|
-
error: Error | null;
|
|
1122
|
-
isLoading: boolean;
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
function useAsync<T>(
|
|
1126
|
-
asyncFn: () => Promise<T>,
|
|
1127
|
-
deps: unknown[] = []
|
|
1128
|
-
): UseAsyncState<T> & { execute: () => Promise<void> } {
|
|
1129
|
-
const [state, setState] = useState<UseAsyncState<T>>({
|
|
1130
|
-
data: null,
|
|
1131
|
-
error: null,
|
|
1132
|
-
isLoading: false,
|
|
1133
|
-
});
|
|
1134
|
-
|
|
1135
|
-
const execute = useCallback(async () => {
|
|
1136
|
-
setState({ data: null, error: null, isLoading: true });
|
|
1137
|
-
try {
|
|
1138
|
-
const result = await asyncFn();
|
|
1139
|
-
setState({ data: result, error: null, isLoading: false });
|
|
1140
|
-
} catch (error) {
|
|
1141
|
-
setState({ data: null, error: error as Error, isLoading: false });
|
|
1142
|
-
}
|
|
1143
|
-
}, deps);
|
|
1144
|
-
|
|
1145
|
-
return { ...state, execute };
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
// Usage
|
|
1149
|
-
function DataComponent() {
|
|
1150
|
-
const { data, error, isLoading, execute } = useAsync(
|
|
1151
|
-
() => fetchData(),
|
|
1152
|
-
[]
|
|
1153
|
-
);
|
|
1154
|
-
|
|
1155
|
-
useEffect(() => {
|
|
1156
|
-
execute();
|
|
1157
|
-
}, [execute]);
|
|
1158
|
-
|
|
1159
|
-
if (isLoading) return <Spinner />;
|
|
1160
|
-
if (error) return <ErrorMessage error={error} onRetry={execute} />;
|
|
1161
|
-
if (!data) return null;
|
|
1162
|
-
|
|
1163
|
-
return <DataDisplay data={data} />;
|
|
1164
|
-
}
|
|
1165
|
-
```
|
|
1166
|
-
|
|
1167
|
-
### Error Fallback Components
|
|
1168
|
-
|
|
1169
|
-
```tsx
|
|
1170
|
-
// CORRECT: Reusable error display components
|
|
1171
|
-
interface ErrorFallbackProps {
|
|
1172
|
-
error: Error;
|
|
1173
|
-
onRetry?: () => void;
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
function ErrorFallback({ error, onRetry }: ErrorFallbackProps) {
|
|
1177
|
-
return (
|
|
1178
|
-
<div role="alert" className="error-fallback">
|
|
1179
|
-
<h2>Something went wrong</h2>
|
|
1180
|
-
<pre>{error.message}</pre>
|
|
1181
|
-
{onRetry && (
|
|
1182
|
-
<button onClick={onRetry}>Try again</button>
|
|
1183
|
-
)}
|
|
1184
|
-
</div>
|
|
1185
|
-
);
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
function NetworkErrorFallback({ error, onRetry }: ErrorFallbackProps) {
|
|
1189
|
-
const isNetworkError = error.message.includes('network') ||
|
|
1190
|
-
error.message.includes('fetch');
|
|
1191
|
-
|
|
1192
|
-
return (
|
|
1193
|
-
<div role="alert" className="network-error">
|
|
1194
|
-
<h2>{isNetworkError ? 'Connection Error' : 'Error'}</h2>
|
|
1195
|
-
<p>
|
|
1196
|
-
{isNetworkError
|
|
1197
|
-
? 'Please check your internet connection'
|
|
1198
|
-
: error.message}
|
|
1199
|
-
</p>
|
|
1200
|
-
{onRetry && (
|
|
1201
|
-
<button onClick={onRetry}>Retry</button>
|
|
1202
|
-
)}
|
|
1203
|
-
</div>
|
|
1204
|
-
);
|
|
1205
|
-
}
|
|
1206
|
-
```
|
|
1207
|
-
|
|
1208
|
-
### Scoped Error Boundaries
|
|
1209
|
-
|
|
1210
|
-
```tsx
|
|
1211
|
-
// CORRECT: Isolate failures to feature boundaries
|
|
1212
|
-
function App() {
|
|
1213
|
-
return (
|
|
1214
|
-
<div>
|
|
1215
|
-
<Header />
|
|
1216
|
-
<main>
|
|
1217
|
-
<ErrorBoundary fallback={<SidebarError />}>
|
|
1218
|
-
<Sidebar />
|
|
1219
|
-
</ErrorBoundary>
|
|
1220
|
-
<ErrorBoundary fallback={<ContentError />}>
|
|
1221
|
-
<MainContent />
|
|
1222
|
-
</ErrorBoundary>
|
|
1223
|
-
</main>
|
|
1224
|
-
<Footer />
|
|
1225
|
-
</div>
|
|
1226
|
-
);
|
|
1227
|
-
}
|
|
1228
|
-
```
|
|
1229
|
-
|
|
1230
|
-
---
|
|
1231
|
-
|
|
1232
|
-
## Performance Patterns
|
|
1233
|
-
|
|
1234
|
-
### Memoized Callbacks
|
|
1235
|
-
|
|
1236
|
-
```tsx
|
|
1237
|
-
// CORRECT: Stable function references for child components
|
|
1238
|
-
function UserList({ users, onSelect }: Props) {
|
|
1239
|
-
const handleSelect = useCallback((userId: string) => {
|
|
1240
|
-
onSelect(userId);
|
|
1241
|
-
}, [onSelect]);
|
|
1242
|
-
|
|
1243
|
-
return <List items={users} onSelect={handleSelect} />;
|
|
1244
|
-
}
|
|
1245
|
-
```
|
|
1246
|
-
|
|
1247
|
-
### Memoized Computed Values
|
|
1248
|
-
|
|
1249
|
-
```tsx
|
|
1250
|
-
// CORRECT: Cache expensive computations
|
|
1251
|
-
function Dashboard({ data }: { data: DataPoint[] }) {
|
|
1252
|
-
const stats = useMemo(() => computeExpensiveStats(data), [data]);
|
|
1253
|
-
const chartData = useMemo(() => transformForChart(data), [data]);
|
|
1254
|
-
|
|
1255
|
-
return (
|
|
1256
|
-
<div>
|
|
1257
|
-
<Stats data={stats} />
|
|
1258
|
-
<Chart data={chartData} />
|
|
1259
|
-
</div>
|
|
1260
|
-
);
|
|
1261
|
-
}
|
|
1262
|
-
```
|
|
1263
|
-
|
|
1264
|
-
### Component Memoization
|
|
1265
|
-
|
|
1266
|
-
```tsx
|
|
1267
|
-
// CORRECT: Skip re-render when props unchanged
|
|
1268
|
-
const UserCard = memo(function UserCard({ user }: { user: User }) {
|
|
1269
|
-
return (
|
|
1270
|
-
<div>
|
|
1271
|
-
<h3>{user.name}</h3>
|
|
1272
|
-
<p>{user.email}</p>
|
|
1273
|
-
</div>
|
|
1274
|
-
);
|
|
1275
|
-
});
|
|
1276
|
-
```
|
|
1277
|
-
|
|
1278
|
-
### Derived State Instead of Effect
|
|
1279
|
-
|
|
1280
|
-
```tsx
|
|
1281
|
-
// CORRECT: Compute during render, not in effect
|
|
1282
|
-
function ProductList({ products, filter }: Props) {
|
|
1283
|
-
const filteredProducts = useMemo(
|
|
1284
|
-
() => products.filter(p => p.category === filter),
|
|
1285
|
-
[products, filter]
|
|
1286
|
-
);
|
|
1287
|
-
|
|
1288
|
-
return <List items={filteredProducts} />;
|
|
1289
|
-
}
|
|
1290
|
-
```
|
|
1291
|
-
|
|
1292
|
-
### Effect Cleanup
|
|
1293
|
-
|
|
1294
|
-
```tsx
|
|
1295
|
-
// CORRECT: Always clean up subscriptions
|
|
1296
|
-
function WindowSize() {
|
|
1297
|
-
const [size, setSize] = useState({ width: 0, height: 0 });
|
|
1298
|
-
|
|
1299
|
-
useEffect(() => {
|
|
1300
|
-
const handleResize = () => {
|
|
1301
|
-
setSize({ width: window.innerWidth, height: window.innerHeight });
|
|
1302
|
-
};
|
|
1303
|
-
|
|
1304
|
-
window.addEventListener('resize', handleResize);
|
|
1305
|
-
return () => window.removeEventListener('resize', handleResize);
|
|
1306
|
-
}, []);
|
|
1307
|
-
|
|
1308
|
-
return <span>{size.width} x {size.height}</span>;
|
|
1309
|
-
}
|
|
1310
|
-
```
|
|
1311
|
-
|
|
1312
|
-
### Debounced Search
|
|
1313
|
-
|
|
1314
|
-
```tsx
|
|
1315
|
-
// CORRECT: Throttle expensive operations
|
|
1316
|
-
function SearchInput({ onSearch }: { onSearch: (term: string) => void }) {
|
|
1317
|
-
const [value, setValue] = useState('');
|
|
1318
|
-
|
|
1319
|
-
const debouncedSearch = useMemo(
|
|
1320
|
-
() => debounce((term: string) => onSearch(term), 300),
|
|
1321
|
-
[onSearch]
|
|
1322
|
-
);
|
|
1323
|
-
|
|
1324
|
-
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
1325
|
-
setValue(e.target.value);
|
|
1326
|
-
debouncedSearch(e.target.value);
|
|
1327
|
-
};
|
|
1328
|
-
|
|
1329
|
-
return <input value={value} onChange={handleChange} />;
|
|
1330
|
-
}
|
|
1331
|
-
```
|