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.
Files changed (134) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +35 -11
  3. package/dist/cli.js +5 -1
  4. package/dist/commands/ambient.d.ts +18 -0
  5. package/dist/commands/ambient.js +136 -0
  6. package/dist/commands/init.d.ts +2 -0
  7. package/dist/commands/init.js +97 -10
  8. package/dist/commands/memory.d.ts +22 -0
  9. package/dist/commands/memory.js +175 -0
  10. package/dist/commands/uninstall.js +72 -5
  11. package/dist/plugins.js +74 -3
  12. package/dist/utils/post-install.d.ts +12 -0
  13. package/dist/utils/post-install.js +82 -1
  14. package/dist/utils/safe-delete-install.d.ts +7 -0
  15. package/dist/utils/safe-delete-install.js +40 -5
  16. package/package.json +2 -1
  17. package/plugins/devflow-accessibility/.claude-plugin/plugin.json +15 -0
  18. package/plugins/devflow-ambient/.claude-plugin/plugin.json +7 -0
  19. package/plugins/devflow-ambient/README.md +49 -0
  20. package/plugins/devflow-ambient/commands/ambient.md +110 -0
  21. package/plugins/devflow-ambient/skills/ambient-router/SKILL.md +89 -0
  22. package/plugins/devflow-ambient/skills/ambient-router/references/skill-catalog.md +68 -0
  23. package/plugins/devflow-audit-claude/.claude-plugin/plugin.json +1 -1
  24. package/plugins/devflow-code-review/.claude-plugin/plugin.json +1 -4
  25. package/plugins/devflow-code-review/agents/reviewer.md +8 -0
  26. package/plugins/devflow-code-review/commands/code-review-teams.md +11 -1
  27. package/plugins/devflow-code-review/commands/code-review.md +12 -2
  28. package/plugins/devflow-core-skills/.claude-plugin/plugin.json +3 -6
  29. package/plugins/devflow-core-skills/skills/docs-framework/SKILL.md +10 -6
  30. package/plugins/devflow-core-skills/skills/test-driven-development/SKILL.md +139 -0
  31. package/plugins/devflow-core-skills/skills/test-driven-development/references/rationalization-prevention.md +111 -0
  32. package/plugins/devflow-debug/.claude-plugin/plugin.json +1 -1
  33. package/plugins/devflow-frontend-design/.claude-plugin/plugin.json +15 -0
  34. package/plugins/devflow-go/.claude-plugin/plugin.json +15 -0
  35. package/plugins/devflow-go/skills/go/SKILL.md +187 -0
  36. package/plugins/devflow-go/skills/go/references/concurrency.md +312 -0
  37. package/plugins/devflow-go/skills/go/references/detection.md +129 -0
  38. package/plugins/devflow-go/skills/go/references/patterns.md +232 -0
  39. package/plugins/devflow-go/skills/go/references/violations.md +205 -0
  40. package/plugins/devflow-implement/.claude-plugin/plugin.json +1 -3
  41. package/plugins/devflow-implement/agents/coder.md +11 -6
  42. package/plugins/devflow-java/.claude-plugin/plugin.json +15 -0
  43. package/plugins/devflow-java/skills/java/SKILL.md +183 -0
  44. package/plugins/devflow-java/skills/java/references/detection.md +120 -0
  45. package/plugins/devflow-java/skills/java/references/modern-java.md +270 -0
  46. package/plugins/devflow-java/skills/java/references/patterns.md +235 -0
  47. package/plugins/devflow-java/skills/java/references/violations.md +213 -0
  48. package/plugins/devflow-python/.claude-plugin/plugin.json +15 -0
  49. package/plugins/devflow-python/skills/python/SKILL.md +188 -0
  50. package/plugins/devflow-python/skills/python/references/async.md +220 -0
  51. package/plugins/devflow-python/skills/python/references/detection.md +128 -0
  52. package/plugins/devflow-python/skills/python/references/patterns.md +226 -0
  53. package/plugins/devflow-python/skills/python/references/violations.md +204 -0
  54. package/plugins/devflow-react/.claude-plugin/plugin.json +15 -0
  55. package/plugins/{devflow-core-skills → devflow-react}/skills/react/SKILL.md +1 -1
  56. package/plugins/{devflow-core-skills → devflow-react}/skills/react/references/patterns.md +3 -3
  57. package/plugins/devflow-resolve/.claude-plugin/plugin.json +1 -1
  58. package/plugins/devflow-rust/.claude-plugin/plugin.json +15 -0
  59. package/plugins/devflow-rust/skills/rust/SKILL.md +193 -0
  60. package/plugins/devflow-rust/skills/rust/references/detection.md +131 -0
  61. package/plugins/devflow-rust/skills/rust/references/ownership.md +242 -0
  62. package/plugins/devflow-rust/skills/rust/references/patterns.md +210 -0
  63. package/plugins/devflow-rust/skills/rust/references/violations.md +191 -0
  64. package/plugins/devflow-self-review/.claude-plugin/plugin.json +1 -1
  65. package/plugins/devflow-specify/.claude-plugin/plugin.json +1 -1
  66. package/plugins/devflow-typescript/.claude-plugin/plugin.json +15 -0
  67. package/plugins/{devflow-core-skills → devflow-typescript}/skills/typescript/references/patterns.md +3 -3
  68. package/scripts/hooks/ambient-prompt.sh +48 -0
  69. package/scripts/hooks/background-memory-update.sh +49 -8
  70. package/scripts/hooks/ensure-memory-gitignore.sh +17 -0
  71. package/scripts/hooks/pre-compact-memory.sh +12 -6
  72. package/scripts/hooks/session-start-memory.sh +50 -8
  73. package/scripts/hooks/stop-update-memory.sh +10 -6
  74. package/shared/agents/coder.md +11 -6
  75. package/shared/agents/reviewer.md +8 -0
  76. package/shared/skills/ambient-router/SKILL.md +89 -0
  77. package/shared/skills/ambient-router/references/skill-catalog.md +68 -0
  78. package/shared/skills/docs-framework/SKILL.md +10 -6
  79. package/shared/skills/go/SKILL.md +187 -0
  80. package/shared/skills/go/references/concurrency.md +312 -0
  81. package/shared/skills/go/references/detection.md +129 -0
  82. package/shared/skills/go/references/patterns.md +232 -0
  83. package/shared/skills/go/references/violations.md +205 -0
  84. package/shared/skills/java/SKILL.md +183 -0
  85. package/shared/skills/java/references/detection.md +120 -0
  86. package/shared/skills/java/references/modern-java.md +270 -0
  87. package/shared/skills/java/references/patterns.md +235 -0
  88. package/shared/skills/java/references/violations.md +213 -0
  89. package/shared/skills/python/SKILL.md +188 -0
  90. package/shared/skills/python/references/async.md +220 -0
  91. package/shared/skills/python/references/detection.md +128 -0
  92. package/shared/skills/python/references/patterns.md +226 -0
  93. package/shared/skills/python/references/violations.md +204 -0
  94. package/shared/skills/react/SKILL.md +1 -1
  95. package/shared/skills/react/references/patterns.md +3 -3
  96. package/shared/skills/rust/SKILL.md +193 -0
  97. package/shared/skills/rust/references/detection.md +131 -0
  98. package/shared/skills/rust/references/ownership.md +242 -0
  99. package/shared/skills/rust/references/patterns.md +210 -0
  100. package/shared/skills/rust/references/violations.md +191 -0
  101. package/shared/skills/test-driven-development/SKILL.md +139 -0
  102. package/shared/skills/test-driven-development/references/rationalization-prevention.md +111 -0
  103. package/shared/skills/typescript/references/patterns.md +3 -3
  104. package/src/templates/managed-settings.json +14 -0
  105. package/plugins/devflow-code-review/skills/react/SKILL.md +0 -276
  106. package/plugins/devflow-code-review/skills/react/references/patterns.md +0 -1331
  107. package/plugins/devflow-core-skills/skills/accessibility/SKILL.md +0 -229
  108. package/plugins/devflow-core-skills/skills/accessibility/references/detection.md +0 -171
  109. package/plugins/devflow-core-skills/skills/accessibility/references/patterns.md +0 -670
  110. package/plugins/devflow-core-skills/skills/accessibility/references/violations.md +0 -419
  111. package/plugins/devflow-core-skills/skills/frontend-design/SKILL.md +0 -254
  112. package/plugins/devflow-core-skills/skills/frontend-design/references/detection.md +0 -184
  113. package/plugins/devflow-core-skills/skills/frontend-design/references/patterns.md +0 -511
  114. package/plugins/devflow-core-skills/skills/frontend-design/references/violations.md +0 -453
  115. package/plugins/devflow-core-skills/skills/react/references/violations.md +0 -565
  116. package/plugins/devflow-implement/skills/accessibility/SKILL.md +0 -229
  117. package/plugins/devflow-implement/skills/accessibility/references/detection.md +0 -171
  118. package/plugins/devflow-implement/skills/accessibility/references/patterns.md +0 -670
  119. package/plugins/devflow-implement/skills/accessibility/references/violations.md +0 -419
  120. package/plugins/devflow-implement/skills/frontend-design/SKILL.md +0 -254
  121. package/plugins/devflow-implement/skills/frontend-design/references/detection.md +0 -184
  122. package/plugins/devflow-implement/skills/frontend-design/references/patterns.md +0 -511
  123. package/plugins/devflow-implement/skills/frontend-design/references/violations.md +0 -453
  124. /package/plugins/{devflow-code-review → devflow-accessibility}/skills/accessibility/SKILL.md +0 -0
  125. /package/plugins/{devflow-code-review → devflow-accessibility}/skills/accessibility/references/detection.md +0 -0
  126. /package/plugins/{devflow-code-review → devflow-accessibility}/skills/accessibility/references/patterns.md +0 -0
  127. /package/plugins/{devflow-code-review → devflow-accessibility}/skills/accessibility/references/violations.md +0 -0
  128. /package/plugins/{devflow-code-review → devflow-frontend-design}/skills/frontend-design/SKILL.md +0 -0
  129. /package/plugins/{devflow-code-review → devflow-frontend-design}/skills/frontend-design/references/detection.md +0 -0
  130. /package/plugins/{devflow-code-review → devflow-frontend-design}/skills/frontend-design/references/patterns.md +0 -0
  131. /package/plugins/{devflow-code-review → devflow-frontend-design}/skills/frontend-design/references/violations.md +0 -0
  132. /package/plugins/{devflow-code-review → devflow-react}/skills/react/references/violations.md +0 -0
  133. /package/plugins/{devflow-core-skills → devflow-typescript}/skills/typescript/SKILL.md +0 -0
  134. /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
- ```