@qazuor/claude-code-config 0.5.0 → 0.6.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/README.md +106 -41
- package/dist/bin.cjs +963 -84
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +963 -84
- package/dist/bin.js.map +1 -1
- package/dist/index.cjs +73 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +73 -56
- package/dist/index.js.map +1 -1
- package/package.json +23 -24
- package/templates/CLAUDE.md.template +60 -5
- package/templates/agents/README.md +58 -39
- package/templates/agents/_registry.json +43 -202
- package/templates/agents/engineering/{hono-engineer.md → api-engineer.md} +61 -70
- package/templates/agents/engineering/database-engineer.md +253 -0
- package/templates/agents/engineering/frontend-engineer.md +302 -0
- package/templates/hooks/on-notification.sh +0 -0
- package/templates/scripts/add-changelogs.sh +0 -0
- package/templates/scripts/generate-code-registry.ts +0 -0
- package/templates/scripts/health-check.sh +0 -0
- package/templates/scripts/sync-registry.sh +0 -0
- package/templates/scripts/telemetry-report.ts +0 -0
- package/templates/scripts/validate-docs.sh +0 -0
- package/templates/scripts/validate-registry.sh +0 -0
- package/templates/scripts/validate-structure.sh +0 -0
- package/templates/scripts/worktree-cleanup.sh +0 -0
- package/templates/scripts/worktree-create.sh +0 -0
- package/templates/skills/README.md +99 -90
- package/templates/skills/_registry.json +323 -16
- package/templates/skills/api-frameworks/express-patterns.md +411 -0
- package/templates/skills/api-frameworks/fastify-patterns.md +419 -0
- package/templates/skills/api-frameworks/hono-patterns.md +388 -0
- package/templates/skills/api-frameworks/nestjs-patterns.md +497 -0
- package/templates/skills/database/drizzle-patterns.md +449 -0
- package/templates/skills/database/mongoose-patterns.md +503 -0
- package/templates/skills/database/prisma-patterns.md +487 -0
- package/templates/skills/frontend-frameworks/astro-patterns.md +415 -0
- package/templates/skills/frontend-frameworks/nextjs-patterns.md +470 -0
- package/templates/skills/frontend-frameworks/react-patterns.md +516 -0
- package/templates/skills/frontend-frameworks/tanstack-start-patterns.md +469 -0
- package/templates/skills/patterns/atdd-methodology.md +364 -0
- package/templates/skills/patterns/bdd-methodology.md +281 -0
- package/templates/skills/patterns/clean-architecture.md +444 -0
- package/templates/skills/patterns/hexagonal-architecture.md +567 -0
- package/templates/skills/patterns/vertical-slice-architecture.md +502 -0
- package/templates/agents/engineering/astro-engineer.md +0 -293
- package/templates/agents/engineering/db-drizzle-engineer.md +0 -360
- package/templates/agents/engineering/express-engineer.md +0 -316
- package/templates/agents/engineering/fastify-engineer.md +0 -399
- package/templates/agents/engineering/mongoose-engineer.md +0 -473
- package/templates/agents/engineering/nestjs-engineer.md +0 -429
- package/templates/agents/engineering/nextjs-engineer.md +0 -451
- package/templates/agents/engineering/prisma-engineer.md +0 -432
- package/templates/agents/engineering/react-senior-dev.md +0 -394
- package/templates/agents/engineering/tanstack-start-engineer.md +0 -447
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
# React Patterns
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
React is a JavaScript library for building user interfaces. This skill provides patterns for building React applications.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Component Patterns
|
|
10
|
+
|
|
11
|
+
### Functional Component with Props
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
/**
|
|
15
|
+
* Item card component
|
|
16
|
+
* Displays item summary with image, title, and price
|
|
17
|
+
*/
|
|
18
|
+
interface ItemCardProps {
|
|
19
|
+
item: Item;
|
|
20
|
+
onSelect?: (id: string) => void;
|
|
21
|
+
priority?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function ItemCard({
|
|
25
|
+
item,
|
|
26
|
+
onSelect,
|
|
27
|
+
priority = false,
|
|
28
|
+
}: ItemCardProps) {
|
|
29
|
+
const handleClick = () => {
|
|
30
|
+
onSelect?.(item.id);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<article
|
|
35
|
+
onClick={handleClick}
|
|
36
|
+
className="cursor-pointer hover:shadow-lg"
|
|
37
|
+
aria-label={`Item: ${item.title}`}
|
|
38
|
+
>
|
|
39
|
+
<img
|
|
40
|
+
src={item.image}
|
|
41
|
+
alt={item.title}
|
|
42
|
+
loading={priority ? 'eager' : 'lazy'}
|
|
43
|
+
/>
|
|
44
|
+
<h3>{item.title}</h3>
|
|
45
|
+
<p>${item.price}</p>
|
|
46
|
+
</article>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Memoized Component
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { memo } from 'react';
|
|
55
|
+
|
|
56
|
+
interface ExpensiveListProps {
|
|
57
|
+
items: Item[];
|
|
58
|
+
onItemSelect: (id: string) => void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function ExpensiveListComponent({ items, onItemSelect }: ExpensiveListProps) {
|
|
62
|
+
return (
|
|
63
|
+
<ul>
|
|
64
|
+
{items.map((item) => (
|
|
65
|
+
<li key={item.id} onClick={() => onItemSelect(item.id)}>
|
|
66
|
+
{item.title}
|
|
67
|
+
</li>
|
|
68
|
+
))}
|
|
69
|
+
</ul>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Memoize to prevent re-renders when props haven't changed
|
|
74
|
+
export const ExpensiveList = memo(ExpensiveListComponent);
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Compound Components
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import { createContext, useContext, useState, type ReactNode } from 'react';
|
|
81
|
+
|
|
82
|
+
// Context
|
|
83
|
+
interface AccordionContextValue {
|
|
84
|
+
openItems: Set<string>;
|
|
85
|
+
toggle: (id: string) => void;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const AccordionContext = createContext<AccordionContextValue | undefined>(undefined);
|
|
89
|
+
|
|
90
|
+
function useAccordion() {
|
|
91
|
+
const context = useContext(AccordionContext);
|
|
92
|
+
if (!context) {
|
|
93
|
+
throw new Error('Accordion components must be used within Accordion');
|
|
94
|
+
}
|
|
95
|
+
return context;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Root
|
|
99
|
+
interface AccordionProps {
|
|
100
|
+
children: ReactNode;
|
|
101
|
+
defaultOpen?: string[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function Accordion({ children, defaultOpen = [] }: AccordionProps) {
|
|
105
|
+
const [openItems, setOpenItems] = useState(new Set(defaultOpen));
|
|
106
|
+
|
|
107
|
+
const toggle = (id: string) => {
|
|
108
|
+
setOpenItems((prev) => {
|
|
109
|
+
const next = new Set(prev);
|
|
110
|
+
if (next.has(id)) {
|
|
111
|
+
next.delete(id);
|
|
112
|
+
} else {
|
|
113
|
+
next.add(id);
|
|
114
|
+
}
|
|
115
|
+
return next;
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<AccordionContext.Provider value={{ openItems, toggle }}>
|
|
121
|
+
<div className="accordion">{children}</div>
|
|
122
|
+
</AccordionContext.Provider>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Item
|
|
127
|
+
interface AccordionItemProps {
|
|
128
|
+
id: string;
|
|
129
|
+
children: ReactNode;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
Accordion.Item = function Item({ id, children }: AccordionItemProps) {
|
|
133
|
+
const { openItems } = useAccordion();
|
|
134
|
+
const isOpen = openItems.has(id);
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<div className="accordion-item" data-open={isOpen}>
|
|
138
|
+
{children}
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// Trigger
|
|
144
|
+
Accordion.Trigger = function Trigger({ id, children }: AccordionItemProps) {
|
|
145
|
+
const { toggle } = useAccordion();
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<button onClick={() => toggle(id)} className="accordion-trigger">
|
|
149
|
+
{children}
|
|
150
|
+
</button>
|
|
151
|
+
);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// Content
|
|
155
|
+
Accordion.Content = function Content({ id, children }: AccordionItemProps) {
|
|
156
|
+
const { openItems } = useAccordion();
|
|
157
|
+
|
|
158
|
+
if (!openItems.has(id)) return null;
|
|
159
|
+
|
|
160
|
+
return <div className="accordion-content">{children}</div>;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export { Accordion };
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Custom Hooks
|
|
169
|
+
|
|
170
|
+
### Data Fetching Hook
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
import { useState, useEffect } from 'react';
|
|
174
|
+
|
|
175
|
+
interface UseFetchResult<T> {
|
|
176
|
+
data: T | null;
|
|
177
|
+
loading: boolean;
|
|
178
|
+
error: Error | null;
|
|
179
|
+
refetch: () => void;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function useFetch<T>(url: string): UseFetchResult<T> {
|
|
183
|
+
const [data, setData] = useState<T | null>(null);
|
|
184
|
+
const [loading, setLoading] = useState(true);
|
|
185
|
+
const [error, setError] = useState<Error | null>(null);
|
|
186
|
+
|
|
187
|
+
const fetchData = async () => {
|
|
188
|
+
setLoading(true);
|
|
189
|
+
setError(null);
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const response = await fetch(url);
|
|
193
|
+
if (!response.ok) {
|
|
194
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
195
|
+
}
|
|
196
|
+
const json = await response.json();
|
|
197
|
+
setData(json);
|
|
198
|
+
} catch (e) {
|
|
199
|
+
setError(e instanceof Error ? e : new Error('Unknown error'));
|
|
200
|
+
} finally {
|
|
201
|
+
setLoading(false);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
fetchData();
|
|
207
|
+
}, [url]);
|
|
208
|
+
|
|
209
|
+
return { data, loading, error, refetch: fetchData };
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Local Storage Hook
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
import { useState, useEffect } from 'react';
|
|
217
|
+
|
|
218
|
+
export function useLocalStorage<T>(
|
|
219
|
+
key: string,
|
|
220
|
+
initialValue: T
|
|
221
|
+
): [T, (value: T | ((prev: T) => T)) => void] {
|
|
222
|
+
const [storedValue, setStoredValue] = useState<T>(() => {
|
|
223
|
+
if (typeof window === 'undefined') {
|
|
224
|
+
return initialValue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const item = window.localStorage.getItem(key);
|
|
229
|
+
return item ? JSON.parse(item) : initialValue;
|
|
230
|
+
} catch {
|
|
231
|
+
return initialValue;
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const setValue = (value: T | ((prev: T) => T)) => {
|
|
236
|
+
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
|
237
|
+
setStoredValue(valueToStore);
|
|
238
|
+
|
|
239
|
+
if (typeof window !== 'undefined') {
|
|
240
|
+
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
return [storedValue, setValue];
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Debounce Hook
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
import { useState, useEffect } from 'react';
|
|
252
|
+
|
|
253
|
+
export function useDebounce<T>(value: T, delay: number): T {
|
|
254
|
+
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
|
255
|
+
|
|
256
|
+
useEffect(() => {
|
|
257
|
+
const handler = setTimeout(() => {
|
|
258
|
+
setDebouncedValue(value);
|
|
259
|
+
}, delay);
|
|
260
|
+
|
|
261
|
+
return () => {
|
|
262
|
+
clearTimeout(handler);
|
|
263
|
+
};
|
|
264
|
+
}, [value, delay]);
|
|
265
|
+
|
|
266
|
+
return debouncedValue;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Usage
|
|
270
|
+
function SearchInput() {
|
|
271
|
+
const [query, setQuery] = useState('');
|
|
272
|
+
const debouncedQuery = useDebounce(query, 300);
|
|
273
|
+
|
|
274
|
+
useEffect(() => {
|
|
275
|
+
if (debouncedQuery) {
|
|
276
|
+
// Perform search
|
|
277
|
+
}
|
|
278
|
+
}, [debouncedQuery]);
|
|
279
|
+
|
|
280
|
+
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## Performance Patterns
|
|
287
|
+
|
|
288
|
+
### useMemo and useCallback
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
import { useMemo, useCallback, useState } from 'react';
|
|
292
|
+
|
|
293
|
+
function ItemList({ items }: { items: Item[] }) {
|
|
294
|
+
const [filter, setFilter] = useState('');
|
|
295
|
+
|
|
296
|
+
// Memoize expensive calculation
|
|
297
|
+
const filteredItems = useMemo(() => {
|
|
298
|
+
return items.filter((item) =>
|
|
299
|
+
item.title.toLowerCase().includes(filter.toLowerCase())
|
|
300
|
+
);
|
|
301
|
+
}, [items, filter]);
|
|
302
|
+
|
|
303
|
+
// Memoize callback to prevent child re-renders
|
|
304
|
+
const handleSelect = useCallback((id: string) => {
|
|
305
|
+
console.log('Selected:', id);
|
|
306
|
+
}, []);
|
|
307
|
+
|
|
308
|
+
return (
|
|
309
|
+
<div>
|
|
310
|
+
<input
|
|
311
|
+
value={filter}
|
|
312
|
+
onChange={(e) => setFilter(e.target.value)}
|
|
313
|
+
placeholder="Filter items..."
|
|
314
|
+
/>
|
|
315
|
+
{filteredItems.map((item) => (
|
|
316
|
+
<ItemCard key={item.id} item={item} onSelect={handleSelect} />
|
|
317
|
+
))}
|
|
318
|
+
</div>
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Code Splitting
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
import { lazy, Suspense } from 'react';
|
|
327
|
+
|
|
328
|
+
// Lazy load components
|
|
329
|
+
const HeavyChart = lazy(() => import('./HeavyChart'));
|
|
330
|
+
const AdminPanel = lazy(() => import('./AdminPanel'));
|
|
331
|
+
|
|
332
|
+
function App() {
|
|
333
|
+
return (
|
|
334
|
+
<Suspense fallback={<LoadingSpinner />}>
|
|
335
|
+
<Routes>
|
|
336
|
+
<Route path="/analytics" element={<HeavyChart />} />
|
|
337
|
+
<Route path="/admin" element={<AdminPanel />} />
|
|
338
|
+
</Routes>
|
|
339
|
+
</Suspense>
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Virtual Lists
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
348
|
+
import { useRef } from 'react';
|
|
349
|
+
|
|
350
|
+
function VirtualList({ items }: { items: Item[] }) {
|
|
351
|
+
const parentRef = useRef<HTMLDivElement>(null);
|
|
352
|
+
|
|
353
|
+
const virtualizer = useVirtualizer({
|
|
354
|
+
count: items.length,
|
|
355
|
+
getScrollElement: () => parentRef.current,
|
|
356
|
+
estimateSize: () => 50,
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
return (
|
|
360
|
+
<div
|
|
361
|
+
ref={parentRef}
|
|
362
|
+
style={{ height: '400px', overflow: 'auto' }}
|
|
363
|
+
>
|
|
364
|
+
<div
|
|
365
|
+
style={{
|
|
366
|
+
height: `${virtualizer.getTotalSize()}px`,
|
|
367
|
+
position: 'relative',
|
|
368
|
+
}}
|
|
369
|
+
>
|
|
370
|
+
{virtualizer.getVirtualItems().map((virtualItem) => (
|
|
371
|
+
<div
|
|
372
|
+
key={virtualItem.key}
|
|
373
|
+
style={{
|
|
374
|
+
position: 'absolute',
|
|
375
|
+
top: 0,
|
|
376
|
+
left: 0,
|
|
377
|
+
width: '100%',
|
|
378
|
+
height: `${virtualItem.size}px`,
|
|
379
|
+
transform: `translateY(${virtualItem.start}px)`,
|
|
380
|
+
}}
|
|
381
|
+
>
|
|
382
|
+
{items[virtualItem.index].title}
|
|
383
|
+
</div>
|
|
384
|
+
))}
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
## Error Handling
|
|
394
|
+
|
|
395
|
+
### Error Boundary
|
|
396
|
+
|
|
397
|
+
```typescript
|
|
398
|
+
import { Component, type ReactNode } from 'react';
|
|
399
|
+
|
|
400
|
+
interface Props {
|
|
401
|
+
children: ReactNode;
|
|
402
|
+
fallback?: ReactNode;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
interface State {
|
|
406
|
+
hasError: boolean;
|
|
407
|
+
error: Error | null;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export class ErrorBoundary extends Component<Props, State> {
|
|
411
|
+
constructor(props: Props) {
|
|
412
|
+
super(props);
|
|
413
|
+
this.state = { hasError: false, error: null };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
static getDerivedStateFromError(error: Error): State {
|
|
417
|
+
return { hasError: true, error };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
|
421
|
+
console.error('Error caught by boundary:', error, errorInfo);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
render() {
|
|
425
|
+
if (this.state.hasError) {
|
|
426
|
+
return this.props.fallback || (
|
|
427
|
+
<div className="error-fallback">
|
|
428
|
+
<h2>Something went wrong</h2>
|
|
429
|
+
<button onClick={() => this.setState({ hasError: false, error: null })}>
|
|
430
|
+
Try again
|
|
431
|
+
</button>
|
|
432
|
+
</div>
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return this.props.children;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
## Testing
|
|
444
|
+
|
|
445
|
+
```typescript
|
|
446
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
447
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
448
|
+
import { ItemCard } from './ItemCard';
|
|
449
|
+
|
|
450
|
+
const mockItem = {
|
|
451
|
+
id: '1',
|
|
452
|
+
title: 'Test Item',
|
|
453
|
+
price: 100,
|
|
454
|
+
image: '/test.jpg',
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
describe('ItemCard', () => {
|
|
458
|
+
it('should render item data', () => {
|
|
459
|
+
render(<ItemCard item={mockItem} />);
|
|
460
|
+
|
|
461
|
+
expect(screen.getByText('Test Item')).toBeInTheDocument();
|
|
462
|
+
expect(screen.getByText('$100')).toBeInTheDocument();
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it('should call onSelect when clicked', () => {
|
|
466
|
+
const onSelect = vi.fn();
|
|
467
|
+
render(<ItemCard item={mockItem} onSelect={onSelect} />);
|
|
468
|
+
|
|
469
|
+
fireEvent.click(screen.getByRole('article'));
|
|
470
|
+
|
|
471
|
+
expect(onSelect).toHaveBeenCalledWith('1');
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('should have proper accessibility attributes', () => {
|
|
475
|
+
render(<ItemCard item={mockItem} />);
|
|
476
|
+
|
|
477
|
+
expect(screen.getByRole('article')).toHaveAttribute(
|
|
478
|
+
'aria-label',
|
|
479
|
+
'Item: Test Item'
|
|
480
|
+
);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it('should use lazy loading by default', () => {
|
|
484
|
+
render(<ItemCard item={mockItem} />);
|
|
485
|
+
|
|
486
|
+
expect(screen.getByRole('img')).toHaveAttribute('loading', 'lazy');
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it('should use eager loading when priority is true', () => {
|
|
490
|
+
render(<ItemCard item={mockItem} priority />);
|
|
491
|
+
|
|
492
|
+
expect(screen.getByRole('img')).toHaveAttribute('loading', 'eager');
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
## Best Practices
|
|
500
|
+
|
|
501
|
+
### Good
|
|
502
|
+
|
|
503
|
+
- Use functional components with hooks
|
|
504
|
+
- Extract reusable logic into custom hooks
|
|
505
|
+
- Memoize expensive calculations and callbacks
|
|
506
|
+
- Use proper TypeScript types for all props
|
|
507
|
+
- Implement error boundaries
|
|
508
|
+
- Use lazy loading for large components
|
|
509
|
+
|
|
510
|
+
### Bad
|
|
511
|
+
|
|
512
|
+
- Inline function definitions in JSX (causes re-renders)
|
|
513
|
+
- Over-memoization (adds overhead when not needed)
|
|
514
|
+
- Prop drilling (use context or state management)
|
|
515
|
+
- Missing loading and error states
|
|
516
|
+
- Not cleaning up effects
|