omgkit 2.2.0 → 2.3.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/package.json +1 -1
- package/plugin/skills/databases/mongodb/SKILL.md +60 -776
- package/plugin/skills/databases/prisma/SKILL.md +53 -744
- package/plugin/skills/databases/redis/SKILL.md +53 -860
- package/plugin/skills/devops/aws/SKILL.md +68 -672
- package/plugin/skills/devops/github-actions/SKILL.md +54 -657
- package/plugin/skills/devops/kubernetes/SKILL.md +67 -602
- package/plugin/skills/devops/performance-profiling/SKILL.md +59 -863
- package/plugin/skills/frameworks/django/SKILL.md +87 -853
- package/plugin/skills/frameworks/express/SKILL.md +95 -1301
- package/plugin/skills/frameworks/fastapi/SKILL.md +90 -1198
- package/plugin/skills/frameworks/laravel/SKILL.md +87 -1187
- package/plugin/skills/frameworks/nestjs/SKILL.md +106 -973
- package/plugin/skills/frameworks/react/SKILL.md +94 -962
- package/plugin/skills/frameworks/vue/SKILL.md +95 -1242
- package/plugin/skills/frontend/accessibility/SKILL.md +91 -1056
- package/plugin/skills/frontend/frontend-design/SKILL.md +69 -1262
- package/plugin/skills/frontend/responsive/SKILL.md +76 -799
- package/plugin/skills/frontend/shadcn-ui/SKILL.md +73 -921
- package/plugin/skills/frontend/tailwindcss/SKILL.md +60 -788
- package/plugin/skills/frontend/threejs/SKILL.md +72 -1266
- package/plugin/skills/languages/javascript/SKILL.md +106 -849
- package/plugin/skills/methodology/brainstorming/SKILL.md +70 -576
- package/plugin/skills/methodology/defense-in-depth/SKILL.md +79 -831
- package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +81 -654
- package/plugin/skills/methodology/executing-plans/SKILL.md +86 -529
- package/plugin/skills/methodology/finishing-development-branch/SKILL.md +95 -586
- package/plugin/skills/methodology/problem-solving/SKILL.md +67 -681
- package/plugin/skills/methodology/receiving-code-review/SKILL.md +70 -533
- package/plugin/skills/methodology/requesting-code-review/SKILL.md +70 -610
- package/plugin/skills/methodology/root-cause-tracing/SKILL.md +70 -646
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +70 -478
- package/plugin/skills/methodology/systematic-debugging/SKILL.md +66 -559
- package/plugin/skills/methodology/test-driven-development/SKILL.md +91 -752
- package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +78 -687
- package/plugin/skills/methodology/token-optimization/SKILL.md +72 -602
- package/plugin/skills/methodology/verification-before-completion/SKILL.md +108 -529
- package/plugin/skills/methodology/writing-plans/SKILL.md +79 -566
- package/plugin/skills/omega/omega-architecture/SKILL.md +91 -752
- package/plugin/skills/omega/omega-coding/SKILL.md +161 -552
- package/plugin/skills/omega/omega-sprint/SKILL.md +132 -777
- package/plugin/skills/omega/omega-testing/SKILL.md +157 -845
- package/plugin/skills/omega/omega-thinking/SKILL.md +165 -606
- package/plugin/skills/security/better-auth/SKILL.md +46 -1034
- package/plugin/skills/security/oauth/SKILL.md +80 -934
- package/plugin/skills/security/owasp/SKILL.md +78 -862
- package/plugin/skills/testing/playwright/SKILL.md +77 -700
- package/plugin/skills/testing/pytest/SKILL.md +73 -811
- package/plugin/skills/testing/vitest/SKILL.md +60 -920
- package/plugin/skills/tools/document-processing/SKILL.md +111 -838
- package/plugin/skills/tools/image-processing/SKILL.md +126 -659
- package/plugin/skills/tools/mcp-development/SKILL.md +85 -758
- package/plugin/skills/tools/media-processing/SKILL.md +118 -735
- package/plugin/stdrules/SKILL_STANDARDS.md +490 -0
- package/plugin/skills/SKILL_STANDARDS.md +0 -743
|
@@ -1,1035 +1,167 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: react
|
|
3
|
-
description: React
|
|
4
|
-
category: frameworks
|
|
5
|
-
triggers:
|
|
6
|
-
- react
|
|
7
|
-
- react component
|
|
8
|
-
- react hooks
|
|
9
|
-
- useState
|
|
10
|
-
- useEffect
|
|
11
|
-
- jsx
|
|
12
|
-
- tsx
|
|
13
|
-
- react app
|
|
2
|
+
name: building-react-apps
|
|
3
|
+
description: Builds production React applications with hooks, TypeScript, state management, and performance optimization. Use when creating React SPAs, component systems, or interactive UIs.
|
|
14
4
|
---
|
|
15
5
|
|
|
16
6
|
# React
|
|
17
7
|
|
|
18
|
-
|
|
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
|
|
8
|
+
## Quick Start
|
|
35
9
|
|
|
36
10
|
```tsx
|
|
37
|
-
|
|
38
|
-
interface ButtonProps {
|
|
39
|
-
variant?: 'primary' | 'secondary' | 'danger';
|
|
40
|
-
size?: 'sm' | 'md' | 'lg';
|
|
41
|
-
disabled?: boolean;
|
|
42
|
-
loading?: boolean;
|
|
43
|
-
onClick?: () => void;
|
|
44
|
-
children: React.ReactNode;
|
|
45
|
-
}
|
|
11
|
+
import { useState } from 'react';
|
|
46
12
|
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
};
|
|
13
|
+
function Counter() {
|
|
14
|
+
const [count, setCount] = useState(0);
|
|
68
15
|
|
|
69
16
|
return (
|
|
70
|
-
<button
|
|
71
|
-
|
|
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
|
-
)}
|
|
17
|
+
<button onClick={() => setCount(c => c + 1)}>
|
|
18
|
+
Count: {count}
|
|
86
19
|
</button>
|
|
87
20
|
);
|
|
88
21
|
}
|
|
89
|
-
|
|
90
|
-
// Card component with composition pattern
|
|
91
|
-
interface CardProps {
|
|
92
|
-
children: React.ReactNode;
|
|
93
|
-
className?: string;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
interface CardHeaderProps {
|
|
97
|
-
title: string;
|
|
98
|
-
subtitle?: string;
|
|
99
|
-
action?: React.ReactNode;
|
|
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
|
-
};
|
|
133
22
|
```
|
|
134
23
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
```tsx
|
|
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]);
|
|
24
|
+
## Features
|
|
246
25
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
26
|
+
| Feature | Description | Guide |
|
|
27
|
+
|---------|-------------|-------|
|
|
28
|
+
| Hooks | useState, useEffect, custom hooks | [HOOKS.md](HOOKS.md) |
|
|
29
|
+
| TypeScript | Props, state, event typing | [TYPESCRIPT.md](TYPESCRIPT.md) |
|
|
30
|
+
| State | Zustand, Context, useReducer | [STATE.md](STATE.md) |
|
|
31
|
+
| Forms | React Hook Form, Zod validation | [FORMS.md](FORMS.md) |
|
|
32
|
+
| Testing | Vitest, Testing Library | [TESTING.md](TESTING.md) |
|
|
33
|
+
| Performance | memo, useMemo, useCallback, Suspense | [PERFORMANCE.md](PERFORMANCE.md) |
|
|
255
34
|
|
|
256
|
-
|
|
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]);
|
|
35
|
+
## Common Patterns
|
|
273
36
|
|
|
274
|
-
|
|
275
|
-
total: filteredData.length,
|
|
276
|
-
active: filteredData.filter(d => d.status === 'active').length,
|
|
277
|
-
inactive: filteredData.filter(d => d.status === 'inactive').length,
|
|
278
|
-
}), [filteredData]);
|
|
37
|
+
### Typed Component with Props
|
|
279
38
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
);
|
|
39
|
+
```tsx
|
|
40
|
+
interface UserCardProps {
|
|
41
|
+
user: { id: string; name: string; email: string };
|
|
42
|
+
onEdit: (user: UserCardProps['user']) => void;
|
|
43
|
+
onDelete: (id: string) => void;
|
|
286
44
|
}
|
|
287
45
|
|
|
288
|
-
|
|
289
|
-
function AutoFocusInput() {
|
|
290
|
-
const inputRef = useRef<HTMLInputElement>(null);
|
|
291
|
-
const renderCount = useRef(0);
|
|
292
|
-
|
|
293
|
-
useEffect(() => {
|
|
294
|
-
// Focus input on mount
|
|
295
|
-
inputRef.current?.focus();
|
|
296
|
-
}, []);
|
|
297
|
-
|
|
298
|
-
useEffect(() => {
|
|
299
|
-
// Track renders without causing re-renders
|
|
300
|
-
renderCount.current += 1;
|
|
301
|
-
});
|
|
302
|
-
|
|
46
|
+
function UserCard({ user, onEdit, onDelete }: UserCardProps) {
|
|
303
47
|
return (
|
|
304
|
-
<div>
|
|
305
|
-
<
|
|
306
|
-
<p>
|
|
48
|
+
<div className="user-card">
|
|
49
|
+
<h3>{user.name}</h3>
|
|
50
|
+
<p>{user.email}</p>
|
|
51
|
+
<button onClick={() => onEdit(user)}>Edit</button>
|
|
52
|
+
<button onClick={() => onDelete(user.id)}>Delete</button>
|
|
307
53
|
</div>
|
|
308
54
|
);
|
|
309
55
|
}
|
|
310
56
|
```
|
|
311
57
|
|
|
312
|
-
###
|
|
58
|
+
### Custom Hook for Data Fetching
|
|
313
59
|
|
|
314
60
|
```tsx
|
|
315
|
-
|
|
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> {
|
|
61
|
+
function useFetch<T>(url: string) {
|
|
324
62
|
const [data, setData] = useState<T | null>(null);
|
|
325
63
|
const [loading, setLoading] = useState(true);
|
|
326
64
|
const [error, setError] = useState<Error | null>(null);
|
|
327
65
|
|
|
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]);
|
|
345
|
-
|
|
346
|
-
useEffect(() => {
|
|
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
|
-
}, []);
|
|
418
|
-
|
|
419
|
-
return size;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// Previous value hook
|
|
423
|
-
function usePrevious<T>(value: T): T | undefined {
|
|
424
|
-
const ref = useRef<T>();
|
|
425
|
-
|
|
426
66
|
useEffect(() => {
|
|
427
|
-
|
|
428
|
-
}, [value]);
|
|
429
|
-
|
|
430
|
-
return ref.current;
|
|
431
|
-
}
|
|
67
|
+
let cancelled = false;
|
|
432
68
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
69
|
+
async function fetchData() {
|
|
70
|
+
try {
|
|
71
|
+
const res = await fetch(url);
|
|
72
|
+
const json = await res.json();
|
|
73
|
+
if (!cancelled) setData(json);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
if (!cancelled) setError(e as Error);
|
|
76
|
+
} finally {
|
|
77
|
+
if (!cancelled) setLoading(false);
|
|
439
78
|
}
|
|
440
79
|
}
|
|
441
80
|
|
|
442
|
-
|
|
443
|
-
return () =>
|
|
444
|
-
}, [
|
|
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';
|
|
81
|
+
fetchData();
|
|
82
|
+
return () => { cancelled = true; };
|
|
83
|
+
}, [url]);
|
|
514
84
|
|
|
515
|
-
|
|
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
|
-
);
|
|
85
|
+
return { data, loading, error };
|
|
544
86
|
}
|
|
545
87
|
```
|
|
546
88
|
|
|
547
|
-
###
|
|
89
|
+
### Form with Validation
|
|
548
90
|
|
|
549
91
|
```tsx
|
|
550
|
-
// Controlled form with validation
|
|
551
92
|
import { useForm } from 'react-hook-form';
|
|
552
93
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
553
94
|
import { z } from 'zod';
|
|
554
95
|
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
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'],
|
|
96
|
+
const schema = z.object({
|
|
97
|
+
email: z.string().email(),
|
|
98
|
+
password: z.string().min(8),
|
|
566
99
|
});
|
|
567
100
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
const {
|
|
572
|
-
register,
|
|
573
|
-
handleSubmit,
|
|
574
|
-
formState: { errors, isSubmitting },
|
|
575
|
-
reset,
|
|
576
|
-
} = useForm<SignUpForm>({
|
|
577
|
-
resolver: zodResolver(signUpSchema),
|
|
101
|
+
function LoginForm() {
|
|
102
|
+
const { register, handleSubmit, formState: { errors } } = useForm({
|
|
103
|
+
resolver: zodResolver(schema),
|
|
578
104
|
});
|
|
579
105
|
|
|
580
|
-
const onSubmit =
|
|
581
|
-
|
|
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
|
-
}
|
|
106
|
+
const onSubmit = (data: z.infer<typeof schema>) => {
|
|
107
|
+
console.log(data);
|
|
595
108
|
};
|
|
596
109
|
|
|
597
110
|
return (
|
|
598
|
-
<form onSubmit={handleSubmit(onSubmit)}
|
|
599
|
-
<
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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>
|
|
111
|
+
<form onSubmit={handleSubmit(onSubmit)}>
|
|
112
|
+
<input {...register('email')} />
|
|
113
|
+
{errors.email && <span>{errors.email.message}</span>}
|
|
114
|
+
<input {...register('password')} type="password" />
|
|
115
|
+
{errors.password && <span>{errors.password.message}</span>}
|
|
116
|
+
<button type="submit">Login</button>
|
|
674
117
|
</form>
|
|
675
118
|
);
|
|
676
119
|
}
|
|
677
120
|
```
|
|
678
121
|
|
|
679
|
-
|
|
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
|
-
}
|
|
122
|
+
## Workflows
|
|
807
123
|
|
|
808
|
-
|
|
809
|
-
return { hasError: true, error };
|
|
810
|
-
}
|
|
124
|
+
### Component Development
|
|
811
125
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
126
|
+
1. Define props interface with TypeScript
|
|
127
|
+
2. Create component with hooks
|
|
128
|
+
3. Extract reusable logic to custom hooks
|
|
129
|
+
4. Add error boundaries for fault isolation
|
|
130
|
+
5. Write tests with Testing Library
|
|
816
131
|
|
|
817
|
-
|
|
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
|
-
}
|
|
132
|
+
### State Management Decision
|
|
836
133
|
|
|
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
134
|
```
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
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
|
-
});
|
|
135
|
+
Local state only -> useState
|
|
136
|
+
Complex local state -> useReducer
|
|
137
|
+
Shared across tree -> Context + useReducer
|
|
138
|
+
App-wide state -> Zustand/Redux
|
|
139
|
+
Server state -> TanStack Query
|
|
947
140
|
```
|
|
948
141
|
|
|
949
|
-
##
|
|
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');
|
|
142
|
+
## Best Practices
|
|
956
143
|
|
|
957
|
-
|
|
144
|
+
| Do | Avoid |
|
|
145
|
+
|----|-------|
|
|
146
|
+
| Use functional components | Class components |
|
|
147
|
+
| Extract custom hooks | Duplicating effect logic |
|
|
148
|
+
| Memoize expensive computations | Premature optimization |
|
|
149
|
+
| Handle loading/error states | Assuming success |
|
|
150
|
+
| Use keys for lists | Index as key for dynamic lists |
|
|
958
151
|
|
|
959
|
-
|
|
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} />
|
|
152
|
+
## Project Structure
|
|
964
153
|
|
|
965
|
-
<div className="col-span-full">
|
|
966
|
-
<ActivityFeed activities={recentActivity || []} />
|
|
967
|
-
</div>
|
|
968
|
-
</div>
|
|
969
|
-
);
|
|
970
|
-
}
|
|
971
154
|
```
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
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
|
-
);
|
|
1004
|
-
}
|
|
155
|
+
src/
|
|
156
|
+
├── App.tsx
|
|
157
|
+
├── main.tsx
|
|
158
|
+
├── components/ # Reusable UI components
|
|
159
|
+
├── hooks/ # Custom hooks
|
|
160
|
+
├── pages/ # Route components
|
|
161
|
+
├── stores/ # State management
|
|
162
|
+
├── services/ # API calls
|
|
163
|
+
├── utils/ # Helper functions
|
|
164
|
+
└── types/ # TypeScript types
|
|
1005
165
|
```
|
|
1006
166
|
|
|
1007
|
-
|
|
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)
|
|
167
|
+
For detailed examples and patterns, see reference files above.
|