claudify-agent 0.0.0-stage → 1.0.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/.claude/QUICK-REFERENCE.md +213 -0
- package/.claude/cost-dashboard.md +162 -0
- package/.claude/docs/COST-OPTIMIZATION.md +403 -0
- package/.claude/docs/MCP-INTEGRATION-GUIDE.md +348 -0
- package/.claude/docs/PLAN-MODE-OPTIMIZATION.md +836 -0
- package/.claude/hooks/auto-learning-review.ts +195 -0
- package/.claude/hooks/auto-lint-fix.ts +104 -0
- package/.claude/hooks/auto-security-review.ts +354 -0
- package/.claude/hooks/auto-validate.ts +348 -0
- package/.claude/hooks/plan-quality-tracker.ts +719 -0
- package/.claude/rules/anti-patterns.md +22 -0
- package/.claude/rules/context-tier-1.md +41 -0
- package/.claude/rules/context-tier-2.md +285 -0
- package/.claude/rules/general.md +25 -0
- package/.claude/rules/mcp-auto-use.md +46 -0
- package/.claude/rules/parallel-exploration-auto.md +31 -0
- package/.claude/rules/plan-mode-execution.md +289 -0
- package/.claude/rules/plan-mode-research.md +34 -0
- package/.claude/rules/plan-mode-structure.md +166 -0
- package/.claude/rules/plan-mode-validation.md +130 -0
- package/.claude/rules/security-critical.md +19 -0
- package/.claude/rules/tool-auto-selection.md +45 -0
- package/.claude/rules/tool-strategy.md +22 -0
- package/.claude/scripts/auto-validate.js +69 -0
- package/.claude/scripts/security-scan.js +45 -0
- package/.claude/security-dashboard.md +142 -0
- package/.claude/skills/backend-concurrency/SKILL.md +203 -0
- package/.claude/skills/debug-optimizer/SKILL.md +399 -0
- package/.claude/skills/debug-optimizer.md +12 -0
- package/.claude/skills/golang-guide/SKILL.md +177 -0
- package/.claude/skills/learning-loop/SKILL.md +308 -0
- package/.claude/skills/mcp-mastery/SKILL.md +214 -0
- package/.claude/skills/nestjs-guide/SKILL.md +197 -0
- package/.claude/skills/nextjs-guide/SKILL.md +687 -0
- package/.claude/skills/nextjs.md +13 -0
- package/.claude/skills/parallel-exploration/SKILL.md +193 -0
- package/.claude/skills/plan-mode-mastery/SKILL.md +263 -0
- package/.claude/skills/python-guide/SKILL.md +822 -0
- package/.claude/skills/react-guide/SKILL.md +762 -0
- package/.claude/skills/react.md +12 -0
- package/.claude/skills/security.md +18 -0
- package/.claude/skills/solidity-guide/SKILL.md +32 -0
- package/.claude/skills/tool-selection/SKILL.md +428 -0
- package/.claude/skills/vite-guide/SKILL.md +445 -0
- package/CLAUDE.md +49 -0
- package/README.md +65 -2
- package/bin/cli.js +50 -0
- package/package.json +44 -4
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: react-guide
|
|
3
|
+
description: Apply React best practices including modern patterns, hooks, TypeScript, performance optimization, and testing. Use when writing, reviewing, or refactoring React code.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# React Best Practices
|
|
7
|
+
|
|
8
|
+
## Quick Start
|
|
9
|
+
|
|
10
|
+
When working with React code, follow these core principles:
|
|
11
|
+
|
|
12
|
+
1. **Modern Patterns**: Use hooks and function components (no class components)
|
|
13
|
+
2. **TypeScript**: Add proper types, avoid `any`
|
|
14
|
+
3. **Server Components**: Use RSC for data fetching when possible
|
|
15
|
+
4. **Performance**: Prevent re-renders, code-split, optimize bundles
|
|
16
|
+
5. **Testing**: Use React Testing Library with user-behavior queries
|
|
17
|
+
6. **Accessibility**: Build accessible components by default
|
|
18
|
+
|
|
19
|
+
## Component Architecture
|
|
20
|
+
|
|
21
|
+
### Function Components with Hooks
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
// ✅ Good: Function component with hooks
|
|
25
|
+
interface UserCardProps {
|
|
26
|
+
userId: string;
|
|
27
|
+
showDetails?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function UserCard({ userId, showDetails = false }: UserCardProps) {
|
|
31
|
+
const [user, setUser] = useState<User | null>(null);
|
|
32
|
+
const [loading, setLoading] = useState(true);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
async function fetchUser() {
|
|
36
|
+
const data = await fetchUserById(userId);
|
|
37
|
+
setUser(data);
|
|
38
|
+
setLoading(false);
|
|
39
|
+
}
|
|
40
|
+
fetchUser();
|
|
41
|
+
}, [userId]);
|
|
42
|
+
|
|
43
|
+
if (loading) return <Spinner />;
|
|
44
|
+
if (!user) return <ErrorMessage />;
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<Card>
|
|
48
|
+
<h2>{user.name}</h2>
|
|
49
|
+
{showDetails && <p>{user.email}</p>}
|
|
50
|
+
</Card>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Custom Hooks for Reusable Logic
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
// ✅ Good: Extract stateful logic into custom hooks
|
|
59
|
+
interface UseFetchOptions {
|
|
60
|
+
enabled?: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function useFetch<T>(
|
|
64
|
+
endpoint: string,
|
|
65
|
+
options: UseFetchOptions = {}
|
|
66
|
+
) {
|
|
67
|
+
const { enabled = true } = options;
|
|
68
|
+
const [data, setData] = useState<T | null>(null);
|
|
69
|
+
const [loading, setLoading] = useState(false);
|
|
70
|
+
const [error, setError] = useState<Error | null>(null);
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (!enabled) return;
|
|
74
|
+
|
|
75
|
+
const controller = new AbortController();
|
|
76
|
+
|
|
77
|
+
async function fetchData() {
|
|
78
|
+
setLoading(true);
|
|
79
|
+
try {
|
|
80
|
+
const response = await fetch(endpoint, {
|
|
81
|
+
signal: controller.signal,
|
|
82
|
+
});
|
|
83
|
+
const result = await response.json();
|
|
84
|
+
setData(result);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
if (err instanceof Error) setError(err);
|
|
87
|
+
} finally {
|
|
88
|
+
setLoading(false);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
fetchData();
|
|
93
|
+
return () => controller.abort();
|
|
94
|
+
}, [endpoint, enabled]);
|
|
95
|
+
|
|
96
|
+
return { data, loading, error };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Usage in component
|
|
100
|
+
function UserProfile({ userId }: { userId: string }) {
|
|
101
|
+
const { data: user, loading, error } = useFetch<User>(
|
|
102
|
+
`/api/users/${userId}`
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
if (loading) return <Spinner />;
|
|
106
|
+
if (error) return <ErrorMessage error={error} />;
|
|
107
|
+
if (!user) return <NotFound />;
|
|
108
|
+
|
|
109
|
+
return <div>{user.name}</div>;
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## React Server Components (RSC)
|
|
114
|
+
|
|
115
|
+
### Server Component Pattern
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
// ✅ Good: Server Component for data fetching
|
|
119
|
+
import { db } from '@/lib/db';
|
|
120
|
+
import { ProductList } from './ProductList';
|
|
121
|
+
|
|
122
|
+
export default async function ProductsPage() {
|
|
123
|
+
const products = await db.product.findMany();
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<div>
|
|
127
|
+
<h1>Products</h1>
|
|
128
|
+
<ProductList products={products} />
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Client Component When Needed
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
// ✅ Good: Client Component only when interactivity needed
|
|
138
|
+
'use client';
|
|
139
|
+
|
|
140
|
+
import { useState } from 'react';
|
|
141
|
+
|
|
142
|
+
interface ProductListProps {
|
|
143
|
+
products: Product[];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function ProductList({ products }: ProductListProps) {
|
|
147
|
+
const [filter, setFilter] = useState('');
|
|
148
|
+
|
|
149
|
+
const filteredProducts = products.filter(p =>
|
|
150
|
+
p.name.toLowerCase().includes(filter.toLowerCase())
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<div>
|
|
155
|
+
<input
|
|
156
|
+
type="text"
|
|
157
|
+
value={filter}
|
|
158
|
+
onChange={e => setFilter(e.target.value)}
|
|
159
|
+
placeholder="Filter products..."
|
|
160
|
+
aria-label="Filter products"
|
|
161
|
+
/>
|
|
162
|
+
<ul>
|
|
163
|
+
{filteredProducts.map(product => (
|
|
164
|
+
<ProductCard key={product.id} product={product} />
|
|
165
|
+
))}
|
|
166
|
+
</ul>
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Performance Optimization
|
|
173
|
+
|
|
174
|
+
### Prevent Unnecessary Re-renders
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
// ✅ Good: React.memo for pure components
|
|
178
|
+
interface ProductCardProps {
|
|
179
|
+
product: Product;
|
|
180
|
+
onAddToCart: (productId: string) => void;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export const ProductCard = React.memo(function ProductCard({
|
|
184
|
+
product,
|
|
185
|
+
onAddToCart,
|
|
186
|
+
}: ProductCardProps) {
|
|
187
|
+
return (
|
|
188
|
+
<div>
|
|
189
|
+
<h3>{product.name}</h3>
|
|
190
|
+
<p>${product.price}</p>
|
|
191
|
+
<button onClick={() => onAddToCart(product.id)}>
|
|
192
|
+
Add to Cart
|
|
193
|
+
</button>
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// ✅ Good: useCallback for stable function references
|
|
199
|
+
function ProductList() {
|
|
200
|
+
const [cart, setCart] = useState<Product[]>([]);
|
|
201
|
+
|
|
202
|
+
const addToCart = useCallback((productId: string) => {
|
|
203
|
+
setCart(prev => [...prev, { id: productId }]);
|
|
204
|
+
}, []);
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<div>
|
|
208
|
+
{products.map(product => (
|
|
209
|
+
<ProductCard
|
|
210
|
+
key={product.id}
|
|
211
|
+
product={product}
|
|
212
|
+
onAddToCart={addToCart}
|
|
213
|
+
/>
|
|
214
|
+
))}
|
|
215
|
+
</div>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ✅ Good: useMemo for expensive calculations
|
|
220
|
+
function ShoppingCart({ items }: { items: CartItem[] }) {
|
|
221
|
+
const total = useMemo(() => {
|
|
222
|
+
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
|
|
223
|
+
}, [items]);
|
|
224
|
+
|
|
225
|
+
return <div>Total: ${total}</div>;
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Code Splitting
|
|
230
|
+
|
|
231
|
+
```tsx
|
|
232
|
+
// ✅ Good: Lazy loading for route-based splitting
|
|
233
|
+
import { lazy, Suspense } from 'react';
|
|
234
|
+
|
|
235
|
+
const ProductDetails = lazy(() => import('./ProductDetails'));
|
|
236
|
+
const Checkout = lazy(() => import('./Checkout'));
|
|
237
|
+
|
|
238
|
+
function App() {
|
|
239
|
+
return (
|
|
240
|
+
<Suspense fallback={<PageLoader />}>
|
|
241
|
+
<Routes>
|
|
242
|
+
<Route path="/products/:id" element={<ProductDetails />} />
|
|
243
|
+
<Route path="/checkout" element={<Checkout />} />
|
|
244
|
+
</Routes>
|
|
245
|
+
</Suspense>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## TypeScript Best Practices
|
|
251
|
+
|
|
252
|
+
### Proper Typing
|
|
253
|
+
|
|
254
|
+
```tsx
|
|
255
|
+
// ✅ Good: Specific types, no any
|
|
256
|
+
interface ButtonProps {
|
|
257
|
+
variant?: 'primary' | 'secondary' | 'danger';
|
|
258
|
+
size?: 'small' | 'medium' | 'large';
|
|
259
|
+
children: React.ReactNode;
|
|
260
|
+
onClick?: () => void;
|
|
261
|
+
disabled?: boolean;
|
|
262
|
+
ariaLabel?: string;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function Button({
|
|
266
|
+
variant = 'primary',
|
|
267
|
+
size = 'medium',
|
|
268
|
+
children,
|
|
269
|
+
onClick,
|
|
270
|
+
disabled = false,
|
|
271
|
+
ariaLabel,
|
|
272
|
+
}: ButtonProps) {
|
|
273
|
+
return (
|
|
274
|
+
<button
|
|
275
|
+
className={`btn btn-${variant} btn-${size}`}
|
|
276
|
+
onClick={onClick}
|
|
277
|
+
disabled={disabled}
|
|
278
|
+
aria-label={ariaLabel}
|
|
279
|
+
>
|
|
280
|
+
{children}
|
|
281
|
+
</button>
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ✅ Good: Type event handlers properly
|
|
286
|
+
interface FormProps {
|
|
287
|
+
onSubmit: (data: FormData) => void;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function Form({ onSubmit }: FormProps) {
|
|
291
|
+
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
|
292
|
+
e.preventDefault();
|
|
293
|
+
const formData = new FormData(e.currentTarget);
|
|
294
|
+
onSubmit({
|
|
295
|
+
email: formData.get('email') as string,
|
|
296
|
+
password: formData.get('password') as string,
|
|
297
|
+
});
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
return (
|
|
301
|
+
<form onSubmit={handleSubmit}>
|
|
302
|
+
<input type="email" name="email" />
|
|
303
|
+
<input type="password" name="password" />
|
|
304
|
+
<button type="submit">Submit</button>
|
|
305
|
+
</form>
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Testing Patterns
|
|
311
|
+
|
|
312
|
+
### React Testing Library
|
|
313
|
+
|
|
314
|
+
```tsx
|
|
315
|
+
// ✅ Good: Test user behavior, not implementation
|
|
316
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
317
|
+
import userEvent from '@testing-library/user-event';
|
|
318
|
+
import { UserProfile } from './UserProfile';
|
|
319
|
+
|
|
320
|
+
describe('UserProfile', () => {
|
|
321
|
+
it('displays user data after loading', async () => {
|
|
322
|
+
render(<UserProfile userId="123" />);
|
|
323
|
+
|
|
324
|
+
expect(screen.getByRole('status')).toHaveTextContent('Loading...');
|
|
325
|
+
|
|
326
|
+
await waitFor(() => {
|
|
327
|
+
expect(screen.getByRole('heading', { name: /john doe/i })).toBeInTheDocument();
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('allows editing user name', async () => {
|
|
332
|
+
const user = userEvent.setup();
|
|
333
|
+
render(<UserProfile userId="123" />);
|
|
334
|
+
|
|
335
|
+
await waitFor(() => {
|
|
336
|
+
expect(screen.getByRole('heading', { name: /john doe/i })).toBeInTheDocument();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const editButton = screen.getByRole('button', { name: /edit/i });
|
|
340
|
+
await user.click(editButton);
|
|
341
|
+
|
|
342
|
+
const nameInput = screen.getByLabelText(/name/i);
|
|
343
|
+
await user.clear(nameInput);
|
|
344
|
+
await user.type(nameInput, 'Jane Doe');
|
|
345
|
+
|
|
346
|
+
const saveButton = screen.getByRole('button', { name: /save/i });
|
|
347
|
+
await user.click(saveButton);
|
|
348
|
+
|
|
349
|
+
await waitFor(() => {
|
|
350
|
+
expect(screen.getByRole('heading', { name: /jane doe/i })).toBeInTheDocument();
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('shows error message when fetch fails', async () => {
|
|
355
|
+
server.use(
|
|
356
|
+
rest.get('/api/users/123', (req, res, ctx) => {
|
|
357
|
+
return res(ctx.status(500));
|
|
358
|
+
})
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
render(<UserProfile userId="123" />);
|
|
362
|
+
|
|
363
|
+
await waitFor(() => {
|
|
364
|
+
expect(screen.getByRole('alert')).toHaveTextContent(/error/i);
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### Testing Custom Hooks
|
|
371
|
+
|
|
372
|
+
```tsx
|
|
373
|
+
// ✅ Good: Test custom hooks in isolation
|
|
374
|
+
import { renderHook, waitFor } from '@testing-library/react';
|
|
375
|
+
import { useFetch } from './useFetch';
|
|
376
|
+
|
|
377
|
+
describe('useFetch', () => {
|
|
378
|
+
it('fetches data successfully', async () => {
|
|
379
|
+
const { result } = renderHook(() => useFetch('/api/users'));
|
|
380
|
+
|
|
381
|
+
expect(result.current.loading).toBe(true);
|
|
382
|
+
|
|
383
|
+
await waitFor(() => {
|
|
384
|
+
expect(result.current.loading).toBe(false);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
expect(result.current.data).toEqual(mockUsers);
|
|
388
|
+
expect(result.current.error).toBeNull();
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('handles fetch error', async () => {
|
|
392
|
+
const { result } = renderHook(() => useFetch('/api/invalid'));
|
|
393
|
+
|
|
394
|
+
await waitFor(() => {
|
|
395
|
+
expect(result.current.loading).toBe(false);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
expect(result.current.error).toBeInstanceOf(Error);
|
|
399
|
+
expect(result.current.data).toBeNull();
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('respects enabled option', () => {
|
|
403
|
+
renderHook(() => useFetch('/api/users', { enabled: false }));
|
|
404
|
+
|
|
405
|
+
expect(fetch).not.toHaveBeenCalled();
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
## Accessibility
|
|
411
|
+
|
|
412
|
+
### Semantic HTML and ARIA
|
|
413
|
+
|
|
414
|
+
```tsx
|
|
415
|
+
// ✅ Good: Semantic HTML with proper ARIA
|
|
416
|
+
interface ModalProps {
|
|
417
|
+
isOpen: boolean;
|
|
418
|
+
onClose: () => void;
|
|
419
|
+
title: string;
|
|
420
|
+
children: React.ReactNode;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export function Modal({ isOpen, onClose, title, children }: ModalProps) {
|
|
424
|
+
useEffect(() => {
|
|
425
|
+
function handleEscape(e: KeyboardEvent) {
|
|
426
|
+
if (e.key === 'Escape') onClose();
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (isOpen) {
|
|
430
|
+
document.addEventListener('keydown', handleEscape);
|
|
431
|
+
document.body.style.overflow = 'hidden';
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return () => {
|
|
435
|
+
document.removeEventListener('keydown', handleEscape);
|
|
436
|
+
document.body.style.overflow = 'unset';
|
|
437
|
+
};
|
|
438
|
+
}, [isOpen, onClose]);
|
|
439
|
+
|
|
440
|
+
if (!isOpen) return null;
|
|
441
|
+
|
|
442
|
+
return (
|
|
443
|
+
<div
|
|
444
|
+
role="dialog"
|
|
445
|
+
aria-modal="true"
|
|
446
|
+
aria-labelledby="modal-title"
|
|
447
|
+
className="modal-overlay"
|
|
448
|
+
onClick={onClose}
|
|
449
|
+
>
|
|
450
|
+
<div className="modal-content" onClick={e => e.stopPropagation()}>
|
|
451
|
+
<header>
|
|
452
|
+
<h2 id="modal-title">{title}</h2>
|
|
453
|
+
<button
|
|
454
|
+
onClick={onClose}
|
|
455
|
+
aria-label="Close modal"
|
|
456
|
+
className="close-button"
|
|
457
|
+
>
|
|
458
|
+
×
|
|
459
|
+
</button>
|
|
460
|
+
</header>
|
|
461
|
+
<main>{children}</main>
|
|
462
|
+
</div>
|
|
463
|
+
</div>
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
### Form Accessibility
|
|
469
|
+
|
|
470
|
+
```tsx
|
|
471
|
+
// ✅ Good: Accessible form with labels and error messages
|
|
472
|
+
interface InputFieldProps {
|
|
473
|
+
id: string;
|
|
474
|
+
label: string;
|
|
475
|
+
error?: string;
|
|
476
|
+
required?: boolean;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export function InputField({
|
|
480
|
+
id,
|
|
481
|
+
label,
|
|
482
|
+
error,
|
|
483
|
+
required = false,
|
|
484
|
+
}: InputFieldProps) {
|
|
485
|
+
return (
|
|
486
|
+
<div className="form-field">
|
|
487
|
+
<label htmlFor={id}>
|
|
488
|
+
{label}
|
|
489
|
+
{required && <span aria-hidden="true"> *</span>}
|
|
490
|
+
</label>
|
|
491
|
+
<input
|
|
492
|
+
id={id}
|
|
493
|
+
name={id}
|
|
494
|
+
aria-required={required}
|
|
495
|
+
aria-invalid={!!error}
|
|
496
|
+
aria-describedby={error ? `${id}-error` : undefined}
|
|
497
|
+
/>
|
|
498
|
+
{error && (
|
|
499
|
+
<span id={`${id}-error`} role="alert" className="error-message">
|
|
500
|
+
{error}
|
|
501
|
+
</span>
|
|
502
|
+
)}
|
|
503
|
+
</div>
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
## State Management
|
|
509
|
+
|
|
510
|
+
### Keep State Minimal
|
|
511
|
+
|
|
512
|
+
```tsx
|
|
513
|
+
// ✅ Good: Minimal state, compute derived values
|
|
514
|
+
interface ShoppingCartProps {
|
|
515
|
+
items: CartItem[];
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
export function ShoppingCart({ items }: ShoppingCartProps) {
|
|
519
|
+
// ✅ Good: Compute derived values during render
|
|
520
|
+
const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
|
|
521
|
+
const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);
|
|
522
|
+
const hasItems = items.length > 0;
|
|
523
|
+
|
|
524
|
+
return (
|
|
525
|
+
<div>
|
|
526
|
+
<p>Items: {itemCount}</p>
|
|
527
|
+
<p>Total: ${total}</p>
|
|
528
|
+
{hasItems ? <Checkout /> : <EmptyCart />}
|
|
529
|
+
</div>
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
### State Colocation
|
|
535
|
+
|
|
536
|
+
```tsx
|
|
537
|
+
// ✅ Good: Colocate state to where it's needed
|
|
538
|
+
function ProductList() {
|
|
539
|
+
const [products] = useFetch<Product[]>('/api/products');
|
|
540
|
+
|
|
541
|
+
return (
|
|
542
|
+
<div>
|
|
543
|
+
{products?.map(product => (
|
|
544
|
+
<ProductCard key={product.id} product={product} />
|
|
545
|
+
))}
|
|
546
|
+
</div>
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function ProductCard({ product }: { product: Product }) {
|
|
551
|
+
// ✅ Good: State only where needed
|
|
552
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
553
|
+
|
|
554
|
+
return (
|
|
555
|
+
<div>
|
|
556
|
+
<h3>{product.name}</h3>
|
|
557
|
+
<p>${product.price}</p>
|
|
558
|
+
{isExpanded && <ProductDetails product={product} />}
|
|
559
|
+
<button onClick={() => setIsExpanded(!isExpanded)}>
|
|
560
|
+
{isExpanded ? 'Show Less' : 'Show More'}
|
|
561
|
+
</button>
|
|
562
|
+
</div>
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
## Error Handling
|
|
568
|
+
|
|
569
|
+
### Error Boundaries
|
|
570
|
+
|
|
571
|
+
```tsx
|
|
572
|
+
// ✅ Good: Error boundary for graceful failures
|
|
573
|
+
interface ErrorBoundaryState {
|
|
574
|
+
hasError: boolean;
|
|
575
|
+
error?: Error;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
export class ErrorBoundary extends React.Component<
|
|
579
|
+
{ children: React.ReactNode; fallback?: React.ComponentType<{ error?: Error }> },
|
|
580
|
+
ErrorBoundaryState
|
|
581
|
+
> {
|
|
582
|
+
state: ErrorBoundaryState = { hasError: false };
|
|
583
|
+
|
|
584
|
+
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
|
585
|
+
return { hasError: true, error };
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
|
589
|
+
console.error('Error caught by boundary:', error, errorInfo);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
render() {
|
|
593
|
+
if (this.state.hasError) {
|
|
594
|
+
const Fallback = this.props.fallback || DefaultErrorFallback;
|
|
595
|
+
return <Fallback error={this.state.error} />;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return this.props.children;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Usage
|
|
603
|
+
function App() {
|
|
604
|
+
return (
|
|
605
|
+
<ErrorBoundary fallback={ProductErrorFallback}>
|
|
606
|
+
<ProductList />
|
|
607
|
+
</ErrorBoundary>
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
## Project Structure
|
|
613
|
+
|
|
614
|
+
### Component Organization
|
|
615
|
+
|
|
616
|
+
```
|
|
617
|
+
src/
|
|
618
|
+
├── components/
|
|
619
|
+
│ ├── ui/ # Reusable UI components
|
|
620
|
+
│ │ ├── Button.tsx
|
|
621
|
+
│ │ ├── Card.tsx
|
|
622
|
+
│ │ └── Modal.tsx
|
|
623
|
+
│ ├── forms/ # Form components
|
|
624
|
+
│ │ ├── Input.tsx
|
|
625
|
+
│ │ ├── Select.tsx
|
|
626
|
+
│ │ └── Form.tsx
|
|
627
|
+
│ └── layout/ # Layout components
|
|
628
|
+
│ ├── Header.tsx
|
|
629
|
+
│ ├── Footer.tsx
|
|
630
|
+
│ └── Layout.tsx
|
|
631
|
+
├── hooks/ # Custom hooks
|
|
632
|
+
│ ├── useFetch.ts
|
|
633
|
+
│ ├── useLocalStorage.ts
|
|
634
|
+
│ └── useDebounce.ts
|
|
635
|
+
├── lib/ # Utilities
|
|
636
|
+
│ ├── api.ts
|
|
637
|
+
│ └── utils.ts
|
|
638
|
+
├── types/ # TypeScript types
|
|
639
|
+
│ └── index.ts
|
|
640
|
+
└── pages/ # Page components (Next.js)
|
|
641
|
+
├── index.tsx
|
|
642
|
+
└── products/
|
|
643
|
+
└── [id].tsx
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
## Common Pitfalls to Avoid
|
|
647
|
+
|
|
648
|
+
### ❌ Bad: Effects as Second Rendering System
|
|
649
|
+
|
|
650
|
+
```tsx
|
|
651
|
+
// ❌ Bad: Computing derived state in effect
|
|
652
|
+
function ShoppingCart({ items }: { items: CartItem[] }) {
|
|
653
|
+
const [total, setTotal] = useState(0);
|
|
654
|
+
|
|
655
|
+
useEffect(() => {
|
|
656
|
+
setTotal(items.reduce((sum, item) => sum + item.price, 0));
|
|
657
|
+
}, [items]);
|
|
658
|
+
|
|
659
|
+
return <div>Total: ${total}</div>;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// ✅ Good: Compute during render
|
|
663
|
+
function ShoppingCart({ items }: { items: CartItem[] }) {
|
|
664
|
+
const total = items.reduce((sum, item) => sum + item.price, 0);
|
|
665
|
+
return <div>Total: ${total}</div>;
|
|
666
|
+
}
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
### ❌ Bad: Missing Cleanup
|
|
670
|
+
|
|
671
|
+
```tsx
|
|
672
|
+
// ❌ Bad: Memory leak from unsubscribed event listener
|
|
673
|
+
function WindowTracker() {
|
|
674
|
+
const [width, setWidth] = useState(window.innerWidth);
|
|
675
|
+
|
|
676
|
+
useEffect(() => {
|
|
677
|
+
window.addEventListener('resize', () => {
|
|
678
|
+
setWidth(window.innerWidth);
|
|
679
|
+
});
|
|
680
|
+
}, []);
|
|
681
|
+
|
|
682
|
+
return <div>Window width: {width}</div>;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// ✅ Good: Proper cleanup
|
|
686
|
+
function WindowTracker() {
|
|
687
|
+
const [width, setWidth] = useState(window.innerWidth);
|
|
688
|
+
|
|
689
|
+
useEffect(() => {
|
|
690
|
+
function handleResize() {
|
|
691
|
+
setWidth(window.innerWidth);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
window.addEventListener('resize', handleResize);
|
|
695
|
+
return () => window.removeEventListener('resize', handleResize);
|
|
696
|
+
}, []);
|
|
697
|
+
|
|
698
|
+
return <div>Window width: {width}</div>;
|
|
699
|
+
}
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
### ❌ Bad: Array/Object in Dependencies
|
|
703
|
+
|
|
704
|
+
```tsx
|
|
705
|
+
// ❌ Bad: Object reference changes every render
|
|
706
|
+
function UserProfile({ userId }: { userId: string }) {
|
|
707
|
+
const config = { timeout: 5000, retries: 3 };
|
|
708
|
+
|
|
709
|
+
useEffect(() => {
|
|
710
|
+
fetchUser(userId, config);
|
|
711
|
+
}, [userId, config]); // config changes every render!
|
|
712
|
+
|
|
713
|
+
return <div>...</div>;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// ✅ Good: Stable reference or primitive values
|
|
717
|
+
function UserProfile({ userId }: { userId: string }) {
|
|
718
|
+
useEffect(() => {
|
|
719
|
+
const config = { timeout: 5000, retries: 3 };
|
|
720
|
+
fetchUser(userId, config);
|
|
721
|
+
}, [userId]);
|
|
722
|
+
|
|
723
|
+
return <div>...</div>;
|
|
724
|
+
}
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
## Commands
|
|
728
|
+
|
|
729
|
+
Run these commands for quality checks:
|
|
730
|
+
|
|
731
|
+
```bash
|
|
732
|
+
npm run lint # ESLint with React hooks plugin
|
|
733
|
+
npm run typecheck # TypeScript compiler
|
|
734
|
+
npm run test # Jest + React Testing Library
|
|
735
|
+
npm run build # Production build with optimizations
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
## Examples
|
|
739
|
+
|
|
740
|
+
### User Authentication Flow
|
|
741
|
+
|
|
742
|
+
See: `@src/components/auth/LoginForm.tsx` for login pattern
|
|
743
|
+
See: `@src/hooks/useAuth.ts` for authentication hook pattern
|
|
744
|
+
See: `@src/components/auth/ProtectedRoute.tsx` for route protection
|
|
745
|
+
|
|
746
|
+
### Data Fetching Pattern
|
|
747
|
+
|
|
748
|
+
See: `@src/hooks/useFetch.ts` for generic fetch hook
|
|
749
|
+
See: `@src/components/products/ProductList.tsx` for server component pattern
|
|
750
|
+
See: `@src/lib/api.ts` for API client setup
|
|
751
|
+
|
|
752
|
+
### Form Handling
|
|
753
|
+
|
|
754
|
+
See: `@src/components/forms/ContactForm.tsx` for controlled form pattern
|
|
755
|
+
See: `@src/components/forms/FormField.tsx` for accessible input pattern
|
|
756
|
+
|
|
757
|
+
## Additional Resources
|
|
758
|
+
|
|
759
|
+
- [React Documentation](https://react.dev) - Official docs with modern patterns
|
|
760
|
+
- [Vercel React Best Practices](https://vercel.com/blog/introducing-react-best-practices) - Performance framework
|
|
761
|
+
- [React Testing Library](https://testing-library.com/react) - Testing guidelines
|
|
762
|
+
- [WebAIM](https://webaim.org) - Accessibility guidelines
|