omgkit 2.0.7 → 2.1.1
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/package.json +2 -2
- package/plugin/skills/backend/api-architecture/SKILL.md +857 -0
- package/plugin/skills/backend/caching-strategies/SKILL.md +755 -0
- package/plugin/skills/backend/event-driven-architecture/SKILL.md +753 -0
- package/plugin/skills/backend/real-time-systems/SKILL.md +635 -0
- package/plugin/skills/databases/database-optimization/SKILL.md +571 -0
- package/plugin/skills/databases/postgresql/SKILL.md +494 -18
- package/plugin/skills/devops/docker/SKILL.md +466 -18
- package/plugin/skills/devops/monorepo-management/SKILL.md +595 -0
- package/plugin/skills/devops/observability/SKILL.md +622 -0
- package/plugin/skills/devops/performance-profiling/SKILL.md +905 -0
- package/plugin/skills/frameworks/nextjs/SKILL.md +407 -44
- package/plugin/skills/frameworks/react/SKILL.md +1006 -32
- package/plugin/skills/frontend/advanced-ui-design/SKILL.md +426 -0
- package/plugin/skills/integrations/ai-integration/SKILL.md +730 -0
- package/plugin/skills/integrations/payment-integration/SKILL.md +735 -0
- package/plugin/skills/languages/python/SKILL.md +489 -25
- package/plugin/skills/languages/typescript/SKILL.md +379 -30
- package/plugin/skills/methodology/problem-solving/SKILL.md +355 -0
- package/plugin/skills/methodology/research-validation/SKILL.md +668 -0
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +260 -0
- package/plugin/skills/mobile/mobile-development/SKILL.md +756 -0
- package/plugin/skills/security/security-hardening/SKILL.md +633 -0
- package/plugin/skills/tools/document-processing/SKILL.md +916 -0
- package/plugin/skills/tools/image-processing/SKILL.md +748 -0
- package/plugin/skills/tools/mcp-development/SKILL.md +883 -0
- package/plugin/skills/tools/media-processing/SKILL.md +831 -0
|
@@ -1,61 +1,1035 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: react
|
|
3
|
-
description: React development
|
|
3
|
+
description: React development with modern patterns, hooks, state management, and performance optimization
|
|
4
|
+
category: frameworks
|
|
5
|
+
triggers:
|
|
6
|
+
- react
|
|
7
|
+
- react component
|
|
8
|
+
- react hooks
|
|
9
|
+
- useState
|
|
10
|
+
- useEffect
|
|
11
|
+
- jsx
|
|
12
|
+
- tsx
|
|
13
|
+
- react app
|
|
4
14
|
---
|
|
5
15
|
|
|
6
|
-
# React
|
|
16
|
+
# React
|
|
7
17
|
|
|
8
|
-
|
|
18
|
+
Modern **React development** following industry best practices. This skill covers functional components, hooks, state management, performance optimization, and testing patterns used by top engineering teams.
|
|
19
|
+
|
|
20
|
+
## Purpose
|
|
21
|
+
|
|
22
|
+
Build maintainable React applications with confidence:
|
|
23
|
+
|
|
24
|
+
- Write clean, reusable functional components
|
|
25
|
+
- Master React hooks for state and side effects
|
|
26
|
+
- Implement efficient state management patterns
|
|
27
|
+
- Optimize performance with memoization
|
|
28
|
+
- Handle forms and validation elegantly
|
|
29
|
+
- Create accessible, responsive UIs
|
|
30
|
+
- Test components effectively
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
|
|
34
|
+
### 1. Functional Components with TypeScript
|
|
9
35
|
|
|
10
|
-
### Functional Component
|
|
11
36
|
```tsx
|
|
37
|
+
// Well-typed component with props interface
|
|
12
38
|
interface ButtonProps {
|
|
13
|
-
|
|
39
|
+
variant?: 'primary' | 'secondary' | 'danger';
|
|
40
|
+
size?: 'sm' | 'md' | 'lg';
|
|
41
|
+
disabled?: boolean;
|
|
42
|
+
loading?: boolean;
|
|
43
|
+
onClick?: () => void;
|
|
44
|
+
children: React.ReactNode;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function Button({
|
|
48
|
+
variant = 'primary',
|
|
49
|
+
size = 'md',
|
|
50
|
+
disabled = false,
|
|
51
|
+
loading = false,
|
|
52
|
+
onClick,
|
|
53
|
+
children,
|
|
54
|
+
}: ButtonProps) {
|
|
55
|
+
const baseStyles = 'rounded font-medium transition-colors focus:outline-none focus:ring-2';
|
|
56
|
+
|
|
57
|
+
const variantStyles = {
|
|
58
|
+
primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
|
|
59
|
+
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500',
|
|
60
|
+
danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const sizeStyles = {
|
|
64
|
+
sm: 'px-3 py-1.5 text-sm',
|
|
65
|
+
md: 'px-4 py-2 text-base',
|
|
66
|
+
lg: 'px-6 py-3 text-lg',
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<button
|
|
71
|
+
type="button"
|
|
72
|
+
disabled={disabled || loading}
|
|
73
|
+
onClick={onClick}
|
|
74
|
+
className={`${baseStyles} ${variantStyles[variant]} ${sizeStyles[size]} ${
|
|
75
|
+
disabled || loading ? 'opacity-50 cursor-not-allowed' : ''
|
|
76
|
+
}`}
|
|
77
|
+
>
|
|
78
|
+
{loading ? (
|
|
79
|
+
<span className="flex items-center gap-2">
|
|
80
|
+
<Spinner size={size} />
|
|
81
|
+
Loading...
|
|
82
|
+
</span>
|
|
83
|
+
) : (
|
|
84
|
+
children
|
|
85
|
+
)}
|
|
86
|
+
</button>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Card component with composition pattern
|
|
91
|
+
interface CardProps {
|
|
14
92
|
children: React.ReactNode;
|
|
93
|
+
className?: string;
|
|
15
94
|
}
|
|
16
95
|
|
|
17
|
-
|
|
18
|
-
|
|
96
|
+
interface CardHeaderProps {
|
|
97
|
+
title: string;
|
|
98
|
+
subtitle?: string;
|
|
99
|
+
action?: React.ReactNode;
|
|
19
100
|
}
|
|
101
|
+
|
|
102
|
+
export function Card({ children, className = '' }: CardProps) {
|
|
103
|
+
return (
|
|
104
|
+
<div className={`bg-white rounded-lg shadow-md overflow-hidden ${className}`}>
|
|
105
|
+
{children}
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
Card.Header = function CardHeader({ title, subtitle, action }: CardHeaderProps) {
|
|
111
|
+
return (
|
|
112
|
+
<div className="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
|
|
113
|
+
<div>
|
|
114
|
+
<h3 className="text-lg font-semibold text-gray-900">{title}</h3>
|
|
115
|
+
{subtitle && <p className="text-sm text-gray-500">{subtitle}</p>}
|
|
116
|
+
</div>
|
|
117
|
+
{action && <div>{action}</div>}
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
Card.Body = function CardBody({ children, className = '' }: CardProps) {
|
|
123
|
+
return <div className={`px-6 py-4 ${className}`}>{children}</div>;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
Card.Footer = function CardFooter({ children, className = '' }: CardProps) {
|
|
127
|
+
return (
|
|
128
|
+
<div className={`px-6 py-4 bg-gray-50 border-t border-gray-200 ${className}`}>
|
|
129
|
+
{children}
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
};
|
|
20
133
|
```
|
|
21
134
|
|
|
22
|
-
### Hooks
|
|
135
|
+
### 2. Essential Hooks Patterns
|
|
136
|
+
|
|
23
137
|
```tsx
|
|
24
|
-
|
|
25
|
-
|
|
138
|
+
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
|
139
|
+
|
|
140
|
+
// useState with complex state
|
|
141
|
+
interface FormState {
|
|
142
|
+
name: string;
|
|
143
|
+
email: string;
|
|
144
|
+
errors: Record<string, string>;
|
|
145
|
+
isSubmitting: boolean;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function useForm(initialValues: Partial<FormState>) {
|
|
149
|
+
const [state, setState] = useState<FormState>({
|
|
150
|
+
name: '',
|
|
151
|
+
email: '',
|
|
152
|
+
errors: {},
|
|
153
|
+
isSubmitting: false,
|
|
154
|
+
...initialValues,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const setField = useCallback((field: keyof FormState, value: unknown) => {
|
|
158
|
+
setState(prev => ({ ...prev, [field]: value }));
|
|
159
|
+
}, []);
|
|
160
|
+
|
|
161
|
+
const setError = useCallback((field: string, error: string) => {
|
|
162
|
+
setState(prev => ({
|
|
163
|
+
...prev,
|
|
164
|
+
errors: { ...prev.errors, [field]: error },
|
|
165
|
+
}));
|
|
166
|
+
}, []);
|
|
167
|
+
|
|
168
|
+
const clearErrors = useCallback(() => {
|
|
169
|
+
setState(prev => ({ ...prev, errors: {} }));
|
|
170
|
+
}, []);
|
|
171
|
+
|
|
172
|
+
return { state, setField, setError, clearErrors };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// useEffect patterns
|
|
176
|
+
function UserProfile({ userId }: { userId: string }) {
|
|
177
|
+
const [user, setUser] = useState<User | null>(null);
|
|
178
|
+
const [loading, setLoading] = useState(true);
|
|
179
|
+
const [error, setError] = useState<Error | null>(null);
|
|
180
|
+
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
let cancelled = false;
|
|
183
|
+
|
|
184
|
+
async function fetchUser() {
|
|
185
|
+
setLoading(true);
|
|
186
|
+
setError(null);
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
const response = await fetch(`/api/users/${userId}`);
|
|
190
|
+
if (!response.ok) throw new Error('Failed to fetch user');
|
|
191
|
+
|
|
192
|
+
const data = await response.json();
|
|
193
|
+
if (!cancelled) {
|
|
194
|
+
setUser(data);
|
|
195
|
+
}
|
|
196
|
+
} catch (err) {
|
|
197
|
+
if (!cancelled) {
|
|
198
|
+
setError(err instanceof Error ? err : new Error('Unknown error'));
|
|
199
|
+
}
|
|
200
|
+
} finally {
|
|
201
|
+
if (!cancelled) {
|
|
202
|
+
setLoading(false);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
fetchUser();
|
|
208
|
+
|
|
209
|
+
// Cleanup function prevents state updates after unmount
|
|
210
|
+
return () => {
|
|
211
|
+
cancelled = true;
|
|
212
|
+
};
|
|
213
|
+
}, [userId]);
|
|
214
|
+
|
|
215
|
+
if (loading) return <LoadingSpinner />;
|
|
216
|
+
if (error) return <ErrorMessage error={error} />;
|
|
217
|
+
if (!user) return null;
|
|
218
|
+
|
|
219
|
+
return <UserCard user={user} />;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// useCallback for stable function references
|
|
223
|
+
function SearchResults({ query }: { query: string }) {
|
|
224
|
+
const [results, setResults] = useState<SearchResult[]>([]);
|
|
225
|
+
|
|
226
|
+
// Memoize the search function to prevent unnecessary re-fetches
|
|
227
|
+
const performSearch = useCallback(async (searchQuery: string) => {
|
|
228
|
+
if (!searchQuery.trim()) {
|
|
229
|
+
setResults([]);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const response = await fetch(`/api/search?q=${encodeURIComponent(searchQuery)}`);
|
|
234
|
+
const data = await response.json();
|
|
235
|
+
setResults(data.results);
|
|
236
|
+
}, []);
|
|
237
|
+
|
|
238
|
+
// Debounce search with useEffect
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
const timeoutId = setTimeout(() => {
|
|
241
|
+
performSearch(query);
|
|
242
|
+
}, 300);
|
|
243
|
+
|
|
244
|
+
return () => clearTimeout(timeoutId);
|
|
245
|
+
}, [query, performSearch]);
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
<ul>
|
|
249
|
+
{results.map(result => (
|
|
250
|
+
<SearchResultItem key={result.id} result={result} />
|
|
251
|
+
))}
|
|
252
|
+
</ul>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// useMemo for expensive computations
|
|
257
|
+
function DataTable({ data, filters }: { data: DataItem[]; filters: Filters }) {
|
|
258
|
+
// Only recompute when data or filters change
|
|
259
|
+
const filteredData = useMemo(() => {
|
|
260
|
+
return data
|
|
261
|
+
.filter(item => {
|
|
262
|
+
if (filters.status && item.status !== filters.status) return false;
|
|
263
|
+
if (filters.search && !item.name.toLowerCase().includes(filters.search.toLowerCase())) {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
return true;
|
|
267
|
+
})
|
|
268
|
+
.sort((a, b) => {
|
|
269
|
+
const direction = filters.sortDirection === 'asc' ? 1 : -1;
|
|
270
|
+
return a[filters.sortBy] > b[filters.sortBy] ? direction : -direction;
|
|
271
|
+
});
|
|
272
|
+
}, [data, filters]);
|
|
273
|
+
|
|
274
|
+
const stats = useMemo(() => ({
|
|
275
|
+
total: filteredData.length,
|
|
276
|
+
active: filteredData.filter(d => d.status === 'active').length,
|
|
277
|
+
inactive: filteredData.filter(d => d.status === 'inactive').length,
|
|
278
|
+
}), [filteredData]);
|
|
279
|
+
|
|
280
|
+
return (
|
|
281
|
+
<div>
|
|
282
|
+
<StatsBar stats={stats} />
|
|
283
|
+
<Table data={filteredData} />
|
|
284
|
+
</div>
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// useRef for DOM access and mutable values
|
|
289
|
+
function AutoFocusInput() {
|
|
290
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
291
|
+
const renderCount = useRef(0);
|
|
26
292
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}, [dependency]);
|
|
293
|
+
useEffect(() => {
|
|
294
|
+
// Focus input on mount
|
|
295
|
+
inputRef.current?.focus();
|
|
296
|
+
}, []);
|
|
32
297
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
298
|
+
useEffect(() => {
|
|
299
|
+
// Track renders without causing re-renders
|
|
300
|
+
renderCount.current += 1;
|
|
301
|
+
});
|
|
37
302
|
|
|
38
|
-
|
|
39
|
-
|
|
303
|
+
return (
|
|
304
|
+
<div>
|
|
305
|
+
<input ref={inputRef} placeholder="I'm auto-focused" />
|
|
306
|
+
<p>Render count: {renderCount.current}</p>
|
|
307
|
+
</div>
|
|
308
|
+
);
|
|
309
|
+
}
|
|
40
310
|
```
|
|
41
311
|
|
|
42
|
-
### Custom
|
|
312
|
+
### 3. Custom Hooks
|
|
313
|
+
|
|
43
314
|
```tsx
|
|
44
|
-
|
|
45
|
-
|
|
315
|
+
// Data fetching hook with error handling and refetch
|
|
316
|
+
interface UseFetchResult<T> {
|
|
317
|
+
data: T | null;
|
|
318
|
+
loading: boolean;
|
|
319
|
+
error: Error | null;
|
|
320
|
+
refetch: () => Promise<void>;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function useFetch<T>(url: string, options?: RequestInit): UseFetchResult<T> {
|
|
324
|
+
const [data, setData] = useState<T | null>(null);
|
|
46
325
|
const [loading, setLoading] = useState(true);
|
|
326
|
+
const [error, setError] = useState<Error | null>(null);
|
|
327
|
+
|
|
328
|
+
const fetchData = useCallback(async () => {
|
|
329
|
+
setLoading(true);
|
|
330
|
+
setError(null);
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
const response = await fetch(url, options);
|
|
334
|
+
if (!response.ok) {
|
|
335
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
336
|
+
}
|
|
337
|
+
const json = await response.json();
|
|
338
|
+
setData(json);
|
|
339
|
+
} catch (e) {
|
|
340
|
+
setError(e instanceof Error ? e : new Error('Fetch failed'));
|
|
341
|
+
} finally {
|
|
342
|
+
setLoading(false);
|
|
343
|
+
}
|
|
344
|
+
}, [url, options]);
|
|
47
345
|
|
|
48
346
|
useEffect(() => {
|
|
49
|
-
|
|
50
|
-
}, [
|
|
347
|
+
fetchData();
|
|
348
|
+
}, [fetchData]);
|
|
349
|
+
|
|
350
|
+
return { data, loading, error, refetch: fetchData };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Local storage hook with SSR support
|
|
354
|
+
function useLocalStorage<T>(key: string, initialValue: T) {
|
|
355
|
+
const [storedValue, setStoredValue] = useState<T>(() => {
|
|
356
|
+
if (typeof window === 'undefined') {
|
|
357
|
+
return initialValue;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
try {
|
|
361
|
+
const item = window.localStorage.getItem(key);
|
|
362
|
+
return item ? JSON.parse(item) : initialValue;
|
|
363
|
+
} catch (error) {
|
|
364
|
+
console.warn(`Error reading localStorage key "${key}":`, error);
|
|
365
|
+
return initialValue;
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
const setValue = useCallback((value: T | ((val: T) => T)) => {
|
|
370
|
+
try {
|
|
371
|
+
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
|
372
|
+
setStoredValue(valueToStore);
|
|
373
|
+
|
|
374
|
+
if (typeof window !== 'undefined') {
|
|
375
|
+
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
|
376
|
+
}
|
|
377
|
+
} catch (error) {
|
|
378
|
+
console.warn(`Error setting localStorage key "${key}":`, error);
|
|
379
|
+
}
|
|
380
|
+
}, [key, storedValue]);
|
|
381
|
+
|
|
382
|
+
return [storedValue, setValue] as const;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Debounced value hook
|
|
386
|
+
function useDebounce<T>(value: T, delay: number): T {
|
|
387
|
+
const [debouncedValue, setDebouncedValue] = useState(value);
|
|
388
|
+
|
|
389
|
+
useEffect(() => {
|
|
390
|
+
const timer = setTimeout(() => {
|
|
391
|
+
setDebouncedValue(value);
|
|
392
|
+
}, delay);
|
|
393
|
+
|
|
394
|
+
return () => clearTimeout(timer);
|
|
395
|
+
}, [value, delay]);
|
|
396
|
+
|
|
397
|
+
return debouncedValue;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Window size hook
|
|
401
|
+
function useWindowSize() {
|
|
402
|
+
const [size, setSize] = useState({
|
|
403
|
+
width: typeof window !== 'undefined' ? window.innerWidth : 0,
|
|
404
|
+
height: typeof window !== 'undefined' ? window.innerHeight : 0,
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
useEffect(() => {
|
|
408
|
+
function handleResize() {
|
|
409
|
+
setSize({
|
|
410
|
+
width: window.innerWidth,
|
|
411
|
+
height: window.innerHeight,
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
window.addEventListener('resize', handleResize);
|
|
416
|
+
return () => window.removeEventListener('resize', handleResize);
|
|
417
|
+
}, []);
|
|
51
418
|
|
|
52
|
-
return
|
|
419
|
+
return size;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Previous value hook
|
|
423
|
+
function usePrevious<T>(value: T): T | undefined {
|
|
424
|
+
const ref = useRef<T>();
|
|
425
|
+
|
|
426
|
+
useEffect(() => {
|
|
427
|
+
ref.current = value;
|
|
428
|
+
}, [value]);
|
|
429
|
+
|
|
430
|
+
return ref.current;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Click outside hook
|
|
434
|
+
function useClickOutside(ref: React.RefObject<HTMLElement>, handler: () => void) {
|
|
435
|
+
useEffect(() => {
|
|
436
|
+
function handleClickOutside(event: MouseEvent) {
|
|
437
|
+
if (ref.current && !ref.current.contains(event.target as Node)) {
|
|
438
|
+
handler();
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
443
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
444
|
+
}, [ref, handler]);
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### 4. State Management Patterns
|
|
449
|
+
|
|
450
|
+
```tsx
|
|
451
|
+
// Context + useReducer for complex state
|
|
452
|
+
interface AppState {
|
|
453
|
+
user: User | null;
|
|
454
|
+
theme: 'light' | 'dark';
|
|
455
|
+
notifications: Notification[];
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
type AppAction =
|
|
459
|
+
| { type: 'SET_USER'; payload: User | null }
|
|
460
|
+
| { type: 'SET_THEME'; payload: 'light' | 'dark' }
|
|
461
|
+
| { type: 'ADD_NOTIFICATION'; payload: Notification }
|
|
462
|
+
| { type: 'REMOVE_NOTIFICATION'; payload: string };
|
|
463
|
+
|
|
464
|
+
const initialState: AppState = {
|
|
465
|
+
user: null,
|
|
466
|
+
theme: 'light',
|
|
467
|
+
notifications: [],
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
function appReducer(state: AppState, action: AppAction): AppState {
|
|
471
|
+
switch (action.type) {
|
|
472
|
+
case 'SET_USER':
|
|
473
|
+
return { ...state, user: action.payload };
|
|
474
|
+
case 'SET_THEME':
|
|
475
|
+
return { ...state, theme: action.payload };
|
|
476
|
+
case 'ADD_NOTIFICATION':
|
|
477
|
+
return { ...state, notifications: [...state.notifications, action.payload] };
|
|
478
|
+
case 'REMOVE_NOTIFICATION':
|
|
479
|
+
return {
|
|
480
|
+
...state,
|
|
481
|
+
notifications: state.notifications.filter(n => n.id !== action.payload),
|
|
482
|
+
};
|
|
483
|
+
default:
|
|
484
|
+
return state;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const AppContext = createContext<{
|
|
489
|
+
state: AppState;
|
|
490
|
+
dispatch: React.Dispatch<AppAction>;
|
|
491
|
+
} | null>(null);
|
|
492
|
+
|
|
493
|
+
export function AppProvider({ children }: { children: React.ReactNode }) {
|
|
494
|
+
const [state, dispatch] = useReducer(appReducer, initialState);
|
|
495
|
+
|
|
496
|
+
return (
|
|
497
|
+
<AppContext.Provider value={{ state, dispatch }}>
|
|
498
|
+
{children}
|
|
499
|
+
</AppContext.Provider>
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export function useAppContext() {
|
|
504
|
+
const context = useContext(AppContext);
|
|
505
|
+
if (!context) {
|
|
506
|
+
throw new Error('useAppContext must be used within AppProvider');
|
|
507
|
+
}
|
|
508
|
+
return context;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Zustand for simpler state management (recommended for most cases)
|
|
512
|
+
import { create } from 'zustand';
|
|
513
|
+
import { persist } from 'zustand/middleware';
|
|
514
|
+
|
|
515
|
+
interface UserStore {
|
|
516
|
+
user: User | null;
|
|
517
|
+
setUser: (user: User | null) => void;
|
|
518
|
+
logout: () => void;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const useUserStore = create<UserStore>()(
|
|
522
|
+
persist(
|
|
523
|
+
(set) => ({
|
|
524
|
+
user: null,
|
|
525
|
+
setUser: (user) => set({ user }),
|
|
526
|
+
logout: () => set({ user: null }),
|
|
527
|
+
}),
|
|
528
|
+
{ name: 'user-storage' }
|
|
529
|
+
)
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
// Usage
|
|
533
|
+
function UserMenu() {
|
|
534
|
+
const { user, logout } = useUserStore();
|
|
535
|
+
|
|
536
|
+
if (!user) return <LoginButton />;
|
|
537
|
+
|
|
538
|
+
return (
|
|
539
|
+
<div>
|
|
540
|
+
<span>Welcome, {user.name}</span>
|
|
541
|
+
<button onClick={logout}>Logout</button>
|
|
542
|
+
</div>
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
### 5. Form Handling
|
|
548
|
+
|
|
549
|
+
```tsx
|
|
550
|
+
// Controlled form with validation
|
|
551
|
+
import { useForm } from 'react-hook-form';
|
|
552
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
553
|
+
import { z } from 'zod';
|
|
554
|
+
|
|
555
|
+
const signUpSchema = z.object({
|
|
556
|
+
name: z.string().min(2, 'Name must be at least 2 characters'),
|
|
557
|
+
email: z.string().email('Invalid email address'),
|
|
558
|
+
password: z.string()
|
|
559
|
+
.min(8, 'Password must be at least 8 characters')
|
|
560
|
+
.regex(/[A-Z]/, 'Password must contain an uppercase letter')
|
|
561
|
+
.regex(/[0-9]/, 'Password must contain a number'),
|
|
562
|
+
confirmPassword: z.string(),
|
|
563
|
+
}).refine(data => data.password === data.confirmPassword, {
|
|
564
|
+
message: "Passwords don't match",
|
|
565
|
+
path: ['confirmPassword'],
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
type SignUpForm = z.infer<typeof signUpSchema>;
|
|
569
|
+
|
|
570
|
+
function SignUpForm() {
|
|
571
|
+
const {
|
|
572
|
+
register,
|
|
573
|
+
handleSubmit,
|
|
574
|
+
formState: { errors, isSubmitting },
|
|
575
|
+
reset,
|
|
576
|
+
} = useForm<SignUpForm>({
|
|
577
|
+
resolver: zodResolver(signUpSchema),
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
const onSubmit = async (data: SignUpForm) => {
|
|
581
|
+
try {
|
|
582
|
+
const response = await fetch('/api/signup', {
|
|
583
|
+
method: 'POST',
|
|
584
|
+
headers: { 'Content-Type': 'application/json' },
|
|
585
|
+
body: JSON.stringify(data),
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
if (!response.ok) throw new Error('Signup failed');
|
|
589
|
+
|
|
590
|
+
reset();
|
|
591
|
+
// Handle success
|
|
592
|
+
} catch (error) {
|
|
593
|
+
// Handle error
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
return (
|
|
598
|
+
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
|
599
|
+
<div>
|
|
600
|
+
<label htmlFor="name" className="block text-sm font-medium">
|
|
601
|
+
Name
|
|
602
|
+
</label>
|
|
603
|
+
<input
|
|
604
|
+
{...register('name')}
|
|
605
|
+
id="name"
|
|
606
|
+
type="text"
|
|
607
|
+
className={`mt-1 block w-full rounded-md border ${
|
|
608
|
+
errors.name ? 'border-red-500' : 'border-gray-300'
|
|
609
|
+
}`}
|
|
610
|
+
/>
|
|
611
|
+
{errors.name && (
|
|
612
|
+
<p className="mt-1 text-sm text-red-500">{errors.name.message}</p>
|
|
613
|
+
)}
|
|
614
|
+
</div>
|
|
615
|
+
|
|
616
|
+
<div>
|
|
617
|
+
<label htmlFor="email" className="block text-sm font-medium">
|
|
618
|
+
Email
|
|
619
|
+
</label>
|
|
620
|
+
<input
|
|
621
|
+
{...register('email')}
|
|
622
|
+
id="email"
|
|
623
|
+
type="email"
|
|
624
|
+
className={`mt-1 block w-full rounded-md border ${
|
|
625
|
+
errors.email ? 'border-red-500' : 'border-gray-300'
|
|
626
|
+
}`}
|
|
627
|
+
/>
|
|
628
|
+
{errors.email && (
|
|
629
|
+
<p className="mt-1 text-sm text-red-500">{errors.email.message}</p>
|
|
630
|
+
)}
|
|
631
|
+
</div>
|
|
632
|
+
|
|
633
|
+
<div>
|
|
634
|
+
<label htmlFor="password" className="block text-sm font-medium">
|
|
635
|
+
Password
|
|
636
|
+
</label>
|
|
637
|
+
<input
|
|
638
|
+
{...register('password')}
|
|
639
|
+
id="password"
|
|
640
|
+
type="password"
|
|
641
|
+
className={`mt-1 block w-full rounded-md border ${
|
|
642
|
+
errors.password ? 'border-red-500' : 'border-gray-300'
|
|
643
|
+
}`}
|
|
644
|
+
/>
|
|
645
|
+
{errors.password && (
|
|
646
|
+
<p className="mt-1 text-sm text-red-500">{errors.password.message}</p>
|
|
647
|
+
)}
|
|
648
|
+
</div>
|
|
649
|
+
|
|
650
|
+
<div>
|
|
651
|
+
<label htmlFor="confirmPassword" className="block text-sm font-medium">
|
|
652
|
+
Confirm Password
|
|
653
|
+
</label>
|
|
654
|
+
<input
|
|
655
|
+
{...register('confirmPassword')}
|
|
656
|
+
id="confirmPassword"
|
|
657
|
+
type="password"
|
|
658
|
+
className={`mt-1 block w-full rounded-md border ${
|
|
659
|
+
errors.confirmPassword ? 'border-red-500' : 'border-gray-300'
|
|
660
|
+
}`}
|
|
661
|
+
/>
|
|
662
|
+
{errors.confirmPassword && (
|
|
663
|
+
<p className="mt-1 text-sm text-red-500">{errors.confirmPassword.message}</p>
|
|
664
|
+
)}
|
|
665
|
+
</div>
|
|
666
|
+
|
|
667
|
+
<button
|
|
668
|
+
type="submit"
|
|
669
|
+
disabled={isSubmitting}
|
|
670
|
+
className="w-full py-2 px-4 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
|
|
671
|
+
>
|
|
672
|
+
{isSubmitting ? 'Signing up...' : 'Sign Up'}
|
|
673
|
+
</button>
|
|
674
|
+
</form>
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
### 6. Performance Optimization
|
|
680
|
+
|
|
681
|
+
```tsx
|
|
682
|
+
import { memo, useMemo, useCallback, lazy, Suspense } from 'react';
|
|
683
|
+
|
|
684
|
+
// Memoized component to prevent unnecessary re-renders
|
|
685
|
+
interface ExpensiveListItemProps {
|
|
686
|
+
item: ListItem;
|
|
687
|
+
onSelect: (id: string) => void;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const ExpensiveListItem = memo(function ExpensiveListItem({
|
|
691
|
+
item,
|
|
692
|
+
onSelect,
|
|
693
|
+
}: ExpensiveListItemProps) {
|
|
694
|
+
// Heavy render logic here
|
|
695
|
+
return (
|
|
696
|
+
<div onClick={() => onSelect(item.id)}>
|
|
697
|
+
{item.name}
|
|
698
|
+
</div>
|
|
699
|
+
);
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
// Parent component with stable callbacks
|
|
703
|
+
function ItemList({ items }: { items: ListItem[] }) {
|
|
704
|
+
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
705
|
+
|
|
706
|
+
// Stable callback reference
|
|
707
|
+
const handleSelect = useCallback((id: string) => {
|
|
708
|
+
setSelectedId(id);
|
|
709
|
+
}, []);
|
|
710
|
+
|
|
711
|
+
return (
|
|
712
|
+
<div>
|
|
713
|
+
{items.map(item => (
|
|
714
|
+
<ExpensiveListItem
|
|
715
|
+
key={item.id}
|
|
716
|
+
item={item}
|
|
717
|
+
onSelect={handleSelect}
|
|
718
|
+
/>
|
|
719
|
+
))}
|
|
720
|
+
</div>
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Lazy loading with code splitting
|
|
725
|
+
const HeavyComponent = lazy(() => import('./HeavyComponent'));
|
|
726
|
+
const AdminDashboard = lazy(() => import('./AdminDashboard'));
|
|
727
|
+
|
|
728
|
+
function App() {
|
|
729
|
+
const [showHeavy, setShowHeavy] = useState(false);
|
|
730
|
+
const isAdmin = useUserStore(state => state.user?.role === 'admin');
|
|
731
|
+
|
|
732
|
+
return (
|
|
733
|
+
<div>
|
|
734
|
+
<button onClick={() => setShowHeavy(true)}>Load Component</button>
|
|
735
|
+
|
|
736
|
+
<Suspense fallback={<LoadingSpinner />}>
|
|
737
|
+
{showHeavy && <HeavyComponent />}
|
|
738
|
+
{isAdmin && <AdminDashboard />}
|
|
739
|
+
</Suspense>
|
|
740
|
+
</div>
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Virtual list for large datasets
|
|
745
|
+
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
746
|
+
|
|
747
|
+
function VirtualList({ items }: { items: Item[] }) {
|
|
748
|
+
const parentRef = useRef<HTMLDivElement>(null);
|
|
749
|
+
|
|
750
|
+
const virtualizer = useVirtualizer({
|
|
751
|
+
count: items.length,
|
|
752
|
+
getScrollElement: () => parentRef.current,
|
|
753
|
+
estimateSize: () => 50,
|
|
754
|
+
overscan: 5,
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
return (
|
|
758
|
+
<div ref={parentRef} className="h-96 overflow-auto">
|
|
759
|
+
<div
|
|
760
|
+
style={{
|
|
761
|
+
height: `${virtualizer.getTotalSize()}px`,
|
|
762
|
+
position: 'relative',
|
|
763
|
+
}}
|
|
764
|
+
>
|
|
765
|
+
{virtualizer.getVirtualItems().map(virtualItem => (
|
|
766
|
+
<div
|
|
767
|
+
key={virtualItem.key}
|
|
768
|
+
style={{
|
|
769
|
+
position: 'absolute',
|
|
770
|
+
top: 0,
|
|
771
|
+
left: 0,
|
|
772
|
+
width: '100%',
|
|
773
|
+
height: `${virtualItem.size}px`,
|
|
774
|
+
transform: `translateY(${virtualItem.start}px)`,
|
|
775
|
+
}}
|
|
776
|
+
>
|
|
777
|
+
{items[virtualItem.index].name}
|
|
778
|
+
</div>
|
|
779
|
+
))}
|
|
780
|
+
</div>
|
|
781
|
+
</div>
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
### 7. Error Boundaries
|
|
787
|
+
|
|
788
|
+
```tsx
|
|
789
|
+
import { Component, ErrorInfo, ReactNode } from 'react';
|
|
790
|
+
|
|
791
|
+
interface ErrorBoundaryProps {
|
|
792
|
+
children: ReactNode;
|
|
793
|
+
fallback?: ReactNode;
|
|
794
|
+
onError?: (error: Error, errorInfo: ErrorInfo) => void;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
interface ErrorBoundaryState {
|
|
798
|
+
hasError: boolean;
|
|
799
|
+
error: Error | null;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
803
|
+
constructor(props: ErrorBoundaryProps) {
|
|
804
|
+
super(props);
|
|
805
|
+
this.state = { hasError: false, error: null };
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
|
809
|
+
return { hasError: true, error };
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
813
|
+
console.error('Error caught by boundary:', error, errorInfo);
|
|
814
|
+
this.props.onError?.(error, errorInfo);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
render() {
|
|
818
|
+
if (this.state.hasError) {
|
|
819
|
+
return this.props.fallback || (
|
|
820
|
+
<div className="p-4 bg-red-50 border border-red-200 rounded-md">
|
|
821
|
+
<h2 className="text-lg font-semibold text-red-800">Something went wrong</h2>
|
|
822
|
+
<p className="text-sm text-red-600">{this.state.error?.message}</p>
|
|
823
|
+
<button
|
|
824
|
+
onClick={() => this.setState({ hasError: false, error: null })}
|
|
825
|
+
className="mt-2 px-3 py-1 bg-red-600 text-white rounded text-sm"
|
|
826
|
+
>
|
|
827
|
+
Try again
|
|
828
|
+
</button>
|
|
829
|
+
</div>
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
return this.props.children;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Usage
|
|
838
|
+
function App() {
|
|
839
|
+
return (
|
|
840
|
+
<ErrorBoundary
|
|
841
|
+
fallback={<ErrorPage />}
|
|
842
|
+
onError={(error) => logErrorToService(error)}
|
|
843
|
+
>
|
|
844
|
+
<MainContent />
|
|
845
|
+
</ErrorBoundary>
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
### 8. Testing Patterns
|
|
851
|
+
|
|
852
|
+
```tsx
|
|
853
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
854
|
+
import userEvent from '@testing-library/user-event';
|
|
855
|
+
import { vi } from 'vitest';
|
|
856
|
+
|
|
857
|
+
// Component test with user events
|
|
858
|
+
describe('SignUpForm', () => {
|
|
859
|
+
it('validates required fields', async () => {
|
|
860
|
+
const user = userEvent.setup();
|
|
861
|
+
render(<SignUpForm />);
|
|
862
|
+
|
|
863
|
+
await user.click(screen.getByRole('button', { name: /sign up/i }));
|
|
864
|
+
|
|
865
|
+
expect(await screen.findByText(/name must be at least 2 characters/i)).toBeInTheDocument();
|
|
866
|
+
expect(screen.getByText(/invalid email address/i)).toBeInTheDocument();
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
it('submits form with valid data', async () => {
|
|
870
|
+
const user = userEvent.setup();
|
|
871
|
+
const mockSubmit = vi.fn();
|
|
872
|
+
|
|
873
|
+
render(<SignUpForm onSubmit={mockSubmit} />);
|
|
874
|
+
|
|
875
|
+
await user.type(screen.getByLabelText(/name/i), 'John Doe');
|
|
876
|
+
await user.type(screen.getByLabelText(/email/i), 'john@example.com');
|
|
877
|
+
await user.type(screen.getByLabelText(/^password$/i), 'Password123');
|
|
878
|
+
await user.type(screen.getByLabelText(/confirm password/i), 'Password123');
|
|
879
|
+
|
|
880
|
+
await user.click(screen.getByRole('button', { name: /sign up/i }));
|
|
881
|
+
|
|
882
|
+
await waitFor(() => {
|
|
883
|
+
expect(mockSubmit).toHaveBeenCalledWith({
|
|
884
|
+
name: 'John Doe',
|
|
885
|
+
email: 'john@example.com',
|
|
886
|
+
password: 'Password123',
|
|
887
|
+
confirmPassword: 'Password123',
|
|
888
|
+
});
|
|
889
|
+
});
|
|
890
|
+
});
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
// Custom hook test
|
|
894
|
+
import { renderHook, act } from '@testing-library/react';
|
|
895
|
+
|
|
896
|
+
describe('useLocalStorage', () => {
|
|
897
|
+
beforeEach(() => {
|
|
898
|
+
localStorage.clear();
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
it('returns initial value when no stored value', () => {
|
|
902
|
+
const { result } = renderHook(() => useLocalStorage('key', 'initial'));
|
|
903
|
+
expect(result.current[0]).toBe('initial');
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
it('updates localStorage when value changes', () => {
|
|
907
|
+
const { result } = renderHook(() => useLocalStorage('key', 'initial'));
|
|
908
|
+
|
|
909
|
+
act(() => {
|
|
910
|
+
result.current[1]('updated');
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
expect(result.current[0]).toBe('updated');
|
|
914
|
+
expect(localStorage.getItem('key')).toBe('"updated"');
|
|
915
|
+
});
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
// Component with async data test
|
|
919
|
+
describe('UserProfile', () => {
|
|
920
|
+
it('shows loading state then user data', async () => {
|
|
921
|
+
const mockUser = { id: '1', name: 'John', email: 'john@example.com' };
|
|
922
|
+
|
|
923
|
+
vi.spyOn(global, 'fetch').mockResolvedValueOnce({
|
|
924
|
+
ok: true,
|
|
925
|
+
json: async () => mockUser,
|
|
926
|
+
} as Response);
|
|
927
|
+
|
|
928
|
+
render(<UserProfile userId="1" />);
|
|
929
|
+
|
|
930
|
+
expect(screen.getByText(/loading/i)).toBeInTheDocument();
|
|
931
|
+
|
|
932
|
+
await waitFor(() => {
|
|
933
|
+
expect(screen.getByText('John')).toBeInTheDocument();
|
|
934
|
+
});
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
it('shows error state on fetch failure', async () => {
|
|
938
|
+
vi.spyOn(global, 'fetch').mockRejectedValueOnce(new Error('Network error'));
|
|
939
|
+
|
|
940
|
+
render(<UserProfile userId="1" />);
|
|
941
|
+
|
|
942
|
+
await waitFor(() => {
|
|
943
|
+
expect(screen.getByText(/error/i)).toBeInTheDocument();
|
|
944
|
+
});
|
|
945
|
+
});
|
|
946
|
+
});
|
|
947
|
+
```
|
|
948
|
+
|
|
949
|
+
## Use Cases
|
|
950
|
+
|
|
951
|
+
### Building a Dashboard
|
|
952
|
+
```tsx
|
|
953
|
+
function Dashboard() {
|
|
954
|
+
const { data: stats, loading } = useFetch<DashboardStats>('/api/stats');
|
|
955
|
+
const { data: recentActivity } = useFetch<Activity[]>('/api/activity');
|
|
956
|
+
|
|
957
|
+
if (loading) return <DashboardSkeleton />;
|
|
958
|
+
|
|
959
|
+
return (
|
|
960
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
961
|
+
<StatsCard title="Total Users" value={stats?.users} />
|
|
962
|
+
<StatsCard title="Revenue" value={`$${stats?.revenue}`} />
|
|
963
|
+
<StatsCard title="Active Projects" value={stats?.projects} />
|
|
964
|
+
|
|
965
|
+
<div className="col-span-full">
|
|
966
|
+
<ActivityFeed activities={recentActivity || []} />
|
|
967
|
+
</div>
|
|
968
|
+
</div>
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
```
|
|
972
|
+
|
|
973
|
+
### Building a Todo App
|
|
974
|
+
```tsx
|
|
975
|
+
function TodoApp() {
|
|
976
|
+
const [todos, setTodos] = useLocalStorage<Todo[]>('todos', []);
|
|
977
|
+
const [filter, setFilter] = useState<'all' | 'active' | 'completed'>('all');
|
|
978
|
+
|
|
979
|
+
const filteredTodos = useMemo(() => {
|
|
980
|
+
switch (filter) {
|
|
981
|
+
case 'active': return todos.filter(t => !t.completed);
|
|
982
|
+
case 'completed': return todos.filter(t => t.completed);
|
|
983
|
+
default: return todos;
|
|
984
|
+
}
|
|
985
|
+
}, [todos, filter]);
|
|
986
|
+
|
|
987
|
+
const addTodo = useCallback((text: string) => {
|
|
988
|
+
setTodos(prev => [...prev, { id: crypto.randomUUID(), text, completed: false }]);
|
|
989
|
+
}, [setTodos]);
|
|
990
|
+
|
|
991
|
+
const toggleTodo = useCallback((id: string) => {
|
|
992
|
+
setTodos(prev => prev.map(t =>
|
|
993
|
+
t.id === id ? { ...t, completed: !t.completed } : t
|
|
994
|
+
));
|
|
995
|
+
}, [setTodos]);
|
|
996
|
+
|
|
997
|
+
return (
|
|
998
|
+
<div>
|
|
999
|
+
<TodoInput onAdd={addTodo} />
|
|
1000
|
+
<FilterButtons filter={filter} onFilterChange={setFilter} />
|
|
1001
|
+
<TodoList todos={filteredTodos} onToggle={toggleTodo} />
|
|
1002
|
+
</div>
|
|
1003
|
+
);
|
|
53
1004
|
}
|
|
54
1005
|
```
|
|
55
1006
|
|
|
56
1007
|
## Best Practices
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
-
|
|
60
|
-
-
|
|
61
|
-
-
|
|
1008
|
+
|
|
1009
|
+
### Do's
|
|
1010
|
+
- Use functional components with hooks
|
|
1011
|
+
- Keep components small and focused
|
|
1012
|
+
- Extract reusable logic into custom hooks
|
|
1013
|
+
- Use TypeScript for better type safety
|
|
1014
|
+
- Memoize expensive computations
|
|
1015
|
+
- Handle loading and error states
|
|
1016
|
+
- Use proper key props for lists
|
|
1017
|
+
- Clean up effects to prevent memory leaks
|
|
1018
|
+
|
|
1019
|
+
### Don'ts
|
|
1020
|
+
- Don't use class components for new code
|
|
1021
|
+
- Don't mutate state directly
|
|
1022
|
+
- Don't overuse useEffect
|
|
1023
|
+
- Don't forget dependency arrays
|
|
1024
|
+
- Don't use index as key for dynamic lists
|
|
1025
|
+
- Don't put business logic in components
|
|
1026
|
+
- Don't skip error boundaries
|
|
1027
|
+
- Don't ignore accessibility
|
|
1028
|
+
|
|
1029
|
+
## References
|
|
1030
|
+
|
|
1031
|
+
- [React Documentation](https://react.dev)
|
|
1032
|
+
- [React TypeScript Cheatsheet](https://react-typescript-cheatsheet.netlify.app)
|
|
1033
|
+
- [Testing Library](https://testing-library.com/docs/react-testing-library/intro)
|
|
1034
|
+
- [Zustand](https://zustand-demo.pmnd.rs)
|
|
1035
|
+
- [React Hook Form](https://react-hook-form.com)
|