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,1325 +1,132 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: frontend-
|
|
3
|
-
description:
|
|
4
|
-
category: frontend
|
|
5
|
-
triggers:
|
|
6
|
-
- frontend design
|
|
7
|
-
- component patterns
|
|
8
|
-
- design patterns
|
|
9
|
-
- compound components
|
|
10
|
-
- render props
|
|
11
|
-
- custom hooks
|
|
12
|
-
- state management
|
|
13
|
-
- component architecture
|
|
2
|
+
name: designing-frontend-patterns
|
|
3
|
+
description: Claude designs scalable React component architectures using compound components, custom hooks, and state machines. Use when building reusable UI systems or complex component APIs.
|
|
14
4
|
---
|
|
15
5
|
|
|
16
|
-
# Frontend
|
|
6
|
+
# Designing Frontend Patterns
|
|
17
7
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
## Purpose
|
|
21
|
-
|
|
22
|
-
Build maintainable and scalable frontend applications:
|
|
23
|
-
|
|
24
|
-
- Design reusable component APIs with compound patterns
|
|
25
|
-
- Implement flexible data fetching with render props
|
|
26
|
-
- Create powerful custom hooks for shared logic
|
|
27
|
-
- Manage complex state with state machines
|
|
28
|
-
- Build accessible form systems
|
|
29
|
-
- Implement optimistic UI updates
|
|
30
|
-
- Design scalable component architecture
|
|
31
|
-
|
|
32
|
-
## Features
|
|
33
|
-
|
|
34
|
-
### 1. Compound Components Pattern
|
|
8
|
+
## Quick Start
|
|
35
9
|
|
|
36
10
|
```tsx
|
|
37
|
-
//
|
|
38
|
-
import { createContext, useContext, useState, useCallback, ReactNode } from 'react';
|
|
39
|
-
|
|
40
|
-
interface SelectContextValue {
|
|
41
|
-
isOpen: boolean;
|
|
42
|
-
selectedValue: string | null;
|
|
43
|
-
selectedLabel: string | null;
|
|
44
|
-
highlightedIndex: number;
|
|
45
|
-
open: () => void;
|
|
46
|
-
close: () => void;
|
|
47
|
-
toggle: () => void;
|
|
48
|
-
selectOption: (value: string, label: string) => void;
|
|
49
|
-
setHighlightedIndex: (index: number) => void;
|
|
50
|
-
}
|
|
51
|
-
|
|
11
|
+
// Compound component pattern with context
|
|
52
12
|
const SelectContext = createContext<SelectContextValue | null>(null);
|
|
53
13
|
|
|
54
|
-
function
|
|
55
|
-
const context = useContext(SelectContext);
|
|
56
|
-
if (!context) {
|
|
57
|
-
throw new Error('Select components must be used within a Select provider');
|
|
58
|
-
}
|
|
59
|
-
return context;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// components/Select/Select.tsx
|
|
63
|
-
interface SelectProps {
|
|
64
|
-
children: ReactNode;
|
|
65
|
-
defaultValue?: string;
|
|
66
|
-
value?: string;
|
|
67
|
-
onValueChange?: (value: string) => void;
|
|
68
|
-
disabled?: boolean;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function Select({
|
|
72
|
-
children,
|
|
73
|
-
defaultValue,
|
|
74
|
-
value: controlledValue,
|
|
75
|
-
onValueChange,
|
|
76
|
-
disabled = false,
|
|
77
|
-
}: SelectProps) {
|
|
14
|
+
export function Select({ children, value, onValueChange }: SelectProps) {
|
|
78
15
|
const [isOpen, setIsOpen] = useState(false);
|
|
79
|
-
const [internalValue, setInternalValue] = useState(defaultValue ?? null);
|
|
80
|
-
const [selectedLabel, setSelectedLabel] = useState<string | null>(null);
|
|
81
|
-
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
|
82
|
-
|
|
83
|
-
const isControlled = controlledValue !== undefined;
|
|
84
|
-
const selectedValue = isControlled ? controlledValue : internalValue;
|
|
85
|
-
|
|
86
|
-
const open = useCallback(() => {
|
|
87
|
-
if (!disabled) setIsOpen(true);
|
|
88
|
-
}, [disabled]);
|
|
89
|
-
|
|
90
|
-
const close = useCallback(() => {
|
|
91
|
-
setIsOpen(false);
|
|
92
|
-
setHighlightedIndex(-1);
|
|
93
|
-
}, []);
|
|
94
|
-
|
|
95
|
-
const toggle = useCallback(() => {
|
|
96
|
-
if (!disabled) setIsOpen((prev) => !prev);
|
|
97
|
-
}, [disabled]);
|
|
98
|
-
|
|
99
|
-
const selectOption = useCallback(
|
|
100
|
-
(value: string, label: string) => {
|
|
101
|
-
if (!isControlled) {
|
|
102
|
-
setInternalValue(value);
|
|
103
|
-
}
|
|
104
|
-
setSelectedLabel(label);
|
|
105
|
-
onValueChange?.(value);
|
|
106
|
-
close();
|
|
107
|
-
},
|
|
108
|
-
[isControlled, onValueChange, close]
|
|
109
|
-
);
|
|
110
|
-
|
|
111
16
|
return (
|
|
112
|
-
<SelectContext.Provider
|
|
113
|
-
|
|
114
|
-
isOpen,
|
|
115
|
-
selectedValue,
|
|
116
|
-
selectedLabel,
|
|
117
|
-
highlightedIndex,
|
|
118
|
-
open,
|
|
119
|
-
close,
|
|
120
|
-
toggle,
|
|
121
|
-
selectOption,
|
|
122
|
-
setHighlightedIndex,
|
|
123
|
-
}}
|
|
124
|
-
>
|
|
125
|
-
<div className="relative inline-block" data-disabled={disabled}>
|
|
126
|
-
{children}
|
|
127
|
-
</div>
|
|
17
|
+
<SelectContext.Provider value={{ isOpen, setIsOpen, value, onValueChange }}>
|
|
18
|
+
<div className="relative">{children}</div>
|
|
128
19
|
</SelectContext.Provider>
|
|
129
20
|
);
|
|
130
21
|
}
|
|
131
22
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
placeholder?: string;
|
|
136
|
-
className?: string;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function SelectTrigger({
|
|
140
|
-
children,
|
|
141
|
-
placeholder = 'Select an option',
|
|
142
|
-
className = '',
|
|
143
|
-
}: SelectTriggerProps) {
|
|
144
|
-
const { isOpen, selectedLabel, toggle } = useSelectContext();
|
|
145
|
-
|
|
146
|
-
return (
|
|
147
|
-
<button
|
|
148
|
-
type="button"
|
|
149
|
-
role="combobox"
|
|
150
|
-
aria-expanded={isOpen}
|
|
151
|
-
aria-haspopup="listbox"
|
|
152
|
-
className={`flex items-center justify-between gap-2 px-3 py-2 border rounded-md
|
|
153
|
-
bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500
|
|
154
|
-
${className}`}
|
|
155
|
-
onClick={toggle}
|
|
156
|
-
>
|
|
157
|
-
<span className={selectedLabel ? 'text-gray-900' : 'text-gray-500'}>
|
|
158
|
-
{selectedLabel || children || placeholder}
|
|
159
|
-
</span>
|
|
160
|
-
<ChevronIcon isOpen={isOpen} />
|
|
161
|
-
</button>
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// components/Select/SelectContent.tsx
|
|
166
|
-
interface SelectContentProps {
|
|
167
|
-
children: ReactNode;
|
|
168
|
-
className?: string;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
export function SelectContent({ children, className = '' }: SelectContentProps) {
|
|
172
|
-
const { isOpen, close } = useSelectContext();
|
|
173
|
-
const contentRef = useRef<HTMLDivElement>(null);
|
|
174
|
-
|
|
175
|
-
// Close on outside click
|
|
176
|
-
useEffect(() => {
|
|
177
|
-
const handleClickOutside = (event: MouseEvent) => {
|
|
178
|
-
if (contentRef.current && !contentRef.current.contains(event.target as Node)) {
|
|
179
|
-
close();
|
|
180
|
-
}
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
if (isOpen) {
|
|
184
|
-
document.addEventListener('mousedown', handleClickOutside);
|
|
185
|
-
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
186
|
-
}
|
|
187
|
-
}, [isOpen, close]);
|
|
188
|
-
|
|
189
|
-
// Close on escape
|
|
190
|
-
useEffect(() => {
|
|
191
|
-
const handleEscape = (event: KeyboardEvent) => {
|
|
192
|
-
if (event.key === 'Escape') close();
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
if (isOpen) {
|
|
196
|
-
document.addEventListener('keydown', handleEscape);
|
|
197
|
-
return () => document.removeEventListener('keydown', handleEscape);
|
|
198
|
-
}
|
|
199
|
-
}, [isOpen, close]);
|
|
200
|
-
|
|
201
|
-
if (!isOpen) return null;
|
|
202
|
-
|
|
203
|
-
return (
|
|
204
|
-
<div
|
|
205
|
-
ref={contentRef}
|
|
206
|
-
role="listbox"
|
|
207
|
-
className={`absolute z-50 mt-1 w-full bg-white border rounded-md shadow-lg
|
|
208
|
-
max-h-60 overflow-auto ${className}`}
|
|
209
|
-
>
|
|
210
|
-
{children}
|
|
211
|
-
</div>
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// components/Select/SelectItem.tsx
|
|
216
|
-
interface SelectItemProps {
|
|
217
|
-
value: string;
|
|
218
|
-
children: ReactNode;
|
|
219
|
-
disabled?: boolean;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
export function SelectItem({ value, children, disabled = false }: SelectItemProps) {
|
|
223
|
-
const { selectedValue, selectOption } = useSelectContext();
|
|
224
|
-
const isSelected = selectedValue === value;
|
|
225
|
-
|
|
226
|
-
const handleSelect = () => {
|
|
227
|
-
if (!disabled) {
|
|
228
|
-
selectOption(value, typeof children === 'string' ? children : value);
|
|
229
|
-
}
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
return (
|
|
233
|
-
<div
|
|
234
|
-
role="option"
|
|
235
|
-
aria-selected={isSelected}
|
|
236
|
-
aria-disabled={disabled}
|
|
237
|
-
className={`px-3 py-2 cursor-pointer
|
|
238
|
-
${isSelected ? 'bg-blue-100 text-blue-900' : 'text-gray-900'}
|
|
239
|
-
${disabled ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-100'}
|
|
240
|
-
`}
|
|
241
|
-
onClick={handleSelect}
|
|
242
|
-
>
|
|
243
|
-
{children}
|
|
244
|
-
</div>
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Usage example
|
|
249
|
-
function SelectExample() {
|
|
250
|
-
const [country, setCountry] = useState('');
|
|
251
|
-
|
|
252
|
-
return (
|
|
253
|
-
<Select value={country} onValueChange={setCountry}>
|
|
254
|
-
<SelectTrigger placeholder="Select a country" />
|
|
255
|
-
<SelectContent>
|
|
256
|
-
<SelectItem value="us">United States</SelectItem>
|
|
257
|
-
<SelectItem value="uk">United Kingdom</SelectItem>
|
|
258
|
-
<SelectItem value="ca">Canada</SelectItem>
|
|
259
|
-
<SelectItem value="au" disabled>Australia (Coming Soon)</SelectItem>
|
|
260
|
-
</SelectContent>
|
|
261
|
-
</Select>
|
|
262
|
-
);
|
|
263
|
-
}
|
|
23
|
+
Select.Trigger = SelectTrigger;
|
|
24
|
+
Select.Content = SelectContent;
|
|
25
|
+
Select.Item = SelectItem;
|
|
264
26
|
```
|
|
265
27
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
```tsx
|
|
269
|
-
// components/DataFetcher.tsx
|
|
270
|
-
import { useState, useEffect, useCallback, ReactNode } from 'react';
|
|
271
|
-
|
|
272
|
-
interface FetchState<T> {
|
|
273
|
-
data: T | null;
|
|
274
|
-
loading: boolean;
|
|
275
|
-
error: Error | null;
|
|
276
|
-
refetch: () => Promise<void>;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
interface DataFetcherProps<T> {
|
|
280
|
-
url: string;
|
|
281
|
-
options?: RequestInit;
|
|
282
|
-
children: (state: FetchState<T>) => ReactNode;
|
|
283
|
-
onSuccess?: (data: T) => void;
|
|
284
|
-
onError?: (error: Error) => void;
|
|
285
|
-
initialData?: T;
|
|
286
|
-
enabled?: boolean;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
export function DataFetcher<T>({
|
|
290
|
-
url,
|
|
291
|
-
options,
|
|
292
|
-
children,
|
|
293
|
-
onSuccess,
|
|
294
|
-
onError,
|
|
295
|
-
initialData = null as T,
|
|
296
|
-
enabled = true,
|
|
297
|
-
}: DataFetcherProps<T>) {
|
|
298
|
-
const [data, setData] = useState<T | null>(initialData);
|
|
299
|
-
const [loading, setLoading] = useState(false);
|
|
300
|
-
const [error, setError] = useState<Error | null>(null);
|
|
301
|
-
|
|
302
|
-
const fetchData = useCallback(async () => {
|
|
303
|
-
if (!enabled) return;
|
|
304
|
-
|
|
305
|
-
setLoading(true);
|
|
306
|
-
setError(null);
|
|
307
|
-
|
|
308
|
-
try {
|
|
309
|
-
const response = await fetch(url, {
|
|
310
|
-
...options,
|
|
311
|
-
headers: {
|
|
312
|
-
'Content-Type': 'application/json',
|
|
313
|
-
...options?.headers,
|
|
314
|
-
},
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
if (!response.ok) {
|
|
318
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
const result = await response.json();
|
|
322
|
-
setData(result);
|
|
323
|
-
onSuccess?.(result);
|
|
324
|
-
} catch (err) {
|
|
325
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
326
|
-
setError(error);
|
|
327
|
-
onError?.(error);
|
|
328
|
-
} finally {
|
|
329
|
-
setLoading(false);
|
|
330
|
-
}
|
|
331
|
-
}, [url, options, enabled, onSuccess, onError]);
|
|
332
|
-
|
|
333
|
-
useEffect(() => {
|
|
334
|
-
fetchData();
|
|
335
|
-
}, [fetchData]);
|
|
336
|
-
|
|
337
|
-
return <>{children({ data, loading, error, refetch: fetchData })}</>;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// Mouse position render props
|
|
341
|
-
interface MousePosition {
|
|
342
|
-
x: number;
|
|
343
|
-
y: number;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
interface MouseTrackerProps {
|
|
347
|
-
children: (position: MousePosition) => ReactNode;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
export function MouseTracker({ children }: MouseTrackerProps) {
|
|
351
|
-
const [position, setPosition] = useState<MousePosition>({ x: 0, y: 0 });
|
|
352
|
-
|
|
353
|
-
useEffect(() => {
|
|
354
|
-
const handleMouseMove = (event: MouseEvent) => {
|
|
355
|
-
setPosition({ x: event.clientX, y: event.clientY });
|
|
356
|
-
};
|
|
357
|
-
|
|
358
|
-
window.addEventListener('mousemove', handleMouseMove);
|
|
359
|
-
return () => window.removeEventListener('mousemove', handleMouseMove);
|
|
360
|
-
}, []);
|
|
361
|
-
|
|
362
|
-
return <>{children(position)}</>;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// Intersection observer render props
|
|
366
|
-
interface IntersectionProps {
|
|
367
|
-
children: (entry: IntersectionObserverEntry | null, ref: React.RefObject<Element>) => ReactNode;
|
|
368
|
-
options?: IntersectionObserverInit;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
export function IntersectionObserverRender({ children, options }: IntersectionProps) {
|
|
372
|
-
const [entry, setEntry] = useState<IntersectionObserverEntry | null>(null);
|
|
373
|
-
const ref = useRef<Element>(null);
|
|
374
|
-
|
|
375
|
-
useEffect(() => {
|
|
376
|
-
const element = ref.current;
|
|
377
|
-
if (!element) return;
|
|
378
|
-
|
|
379
|
-
const observer = new IntersectionObserver(([entry]) => {
|
|
380
|
-
setEntry(entry);
|
|
381
|
-
}, options);
|
|
382
|
-
|
|
383
|
-
observer.observe(element);
|
|
384
|
-
return () => observer.disconnect();
|
|
385
|
-
}, [options]);
|
|
386
|
-
|
|
387
|
-
return <>{children(entry, ref)}</>;
|
|
388
|
-
}
|
|
28
|
+
## Features
|
|
389
29
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
if (error) return <ErrorMessage error={error} onRetry={refetch} />;
|
|
399
|
-
return <UserList users={data || []} />;
|
|
400
|
-
}}
|
|
401
|
-
</DataFetcher>
|
|
30
|
+
| Feature | Description | Guide |
|
|
31
|
+
|---------|-------------|-------|
|
|
32
|
+
| Compound Components | Shared state via context for flexible component APIs | `ref/compound-components.md` |
|
|
33
|
+
| Custom Hooks | Encapsulate reusable logic (useDebounce, useLocalStorage) | `ref/custom-hooks.md` |
|
|
34
|
+
| Render Props | Maximum flexibility for data fetching and rendering | `ref/render-props.md` |
|
|
35
|
+
| State Machines | Predictable state transitions for complex flows | `ref/state-machines.md` |
|
|
36
|
+
| HOCs | Cross-cutting concerns (auth, error boundaries) | `ref/higher-order-components.md` |
|
|
37
|
+
| Optimistic UI | Instant feedback with rollback on failure | `ref/optimistic-updates.md` |
|
|
402
38
|
|
|
403
|
-
|
|
404
|
-
<MouseTracker>
|
|
405
|
-
{({ x, y }) => (
|
|
406
|
-
<div className="fixed pointer-events-none" style={{ left: x, top: y }}>
|
|
407
|
-
Custom cursor at ({x}, {y})
|
|
408
|
-
</div>
|
|
409
|
-
)}
|
|
410
|
-
</MouseTracker>
|
|
39
|
+
## Common Patterns
|
|
411
40
|
|
|
412
|
-
|
|
413
|
-
<IntersectionObserverRender options={{ threshold: 0.1 }}>
|
|
414
|
-
{(entry, ref) => (
|
|
415
|
-
<div ref={ref as React.RefObject<HTMLDivElement>}>
|
|
416
|
-
{entry?.isIntersecting ? <HeavyComponent /> : <Placeholder />}
|
|
417
|
-
</div>
|
|
418
|
-
)}
|
|
419
|
-
</IntersectionObserverRender>
|
|
420
|
-
</>
|
|
421
|
-
);
|
|
422
|
-
}
|
|
423
|
-
```
|
|
424
|
-
|
|
425
|
-
### 3. Custom Hooks Pattern
|
|
41
|
+
### Custom Hook with Cleanup
|
|
426
42
|
|
|
427
43
|
```tsx
|
|
428
|
-
// hooks/useLocalStorage.ts
|
|
429
|
-
import { useState, useEffect, useCallback } from 'react';
|
|
430
|
-
|
|
431
|
-
export function useLocalStorage<T>(
|
|
432
|
-
key: string,
|
|
433
|
-
initialValue: T
|
|
434
|
-
): [T, (value: T | ((prev: T) => T)) => void, () => void] {
|
|
435
|
-
// Get initial value from localStorage or use provided initial
|
|
436
|
-
const [storedValue, setStoredValue] = useState<T>(() => {
|
|
437
|
-
if (typeof window === 'undefined') return initialValue;
|
|
438
|
-
|
|
439
|
-
try {
|
|
440
|
-
const item = window.localStorage.getItem(key);
|
|
441
|
-
return item ? JSON.parse(item) : initialValue;
|
|
442
|
-
} catch (error) {
|
|
443
|
-
console.warn(`Error reading localStorage key "${key}":`, error);
|
|
444
|
-
return initialValue;
|
|
445
|
-
}
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
// Persist to localStorage when value changes
|
|
449
|
-
useEffect(() => {
|
|
450
|
-
if (typeof window === 'undefined') return;
|
|
451
|
-
|
|
452
|
-
try {
|
|
453
|
-
window.localStorage.setItem(key, JSON.stringify(storedValue));
|
|
454
|
-
} catch (error) {
|
|
455
|
-
console.warn(`Error setting localStorage key "${key}":`, error);
|
|
456
|
-
}
|
|
457
|
-
}, [key, storedValue]);
|
|
458
|
-
|
|
459
|
-
// Listen for changes in other tabs/windows
|
|
460
|
-
useEffect(() => {
|
|
461
|
-
const handleStorageChange = (event: StorageEvent) => {
|
|
462
|
-
if (event.key === key && event.newValue) {
|
|
463
|
-
try {
|
|
464
|
-
setStoredValue(JSON.parse(event.newValue));
|
|
465
|
-
} catch {
|
|
466
|
-
// Ignore parse errors
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
};
|
|
470
|
-
|
|
471
|
-
window.addEventListener('storage', handleStorageChange);
|
|
472
|
-
return () => window.removeEventListener('storage', handleStorageChange);
|
|
473
|
-
}, [key]);
|
|
474
|
-
|
|
475
|
-
const remove = useCallback(() => {
|
|
476
|
-
if (typeof window !== 'undefined') {
|
|
477
|
-
window.localStorage.removeItem(key);
|
|
478
|
-
setStoredValue(initialValue);
|
|
479
|
-
}
|
|
480
|
-
}, [key, initialValue]);
|
|
481
|
-
|
|
482
|
-
return [storedValue, setStoredValue, remove];
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
// hooks/useDebounce.ts
|
|
486
44
|
export function useDebounce<T>(value: T, delay: number): T {
|
|
487
45
|
const [debouncedValue, setDebouncedValue] = useState(value);
|
|
488
46
|
|
|
489
47
|
useEffect(() => {
|
|
490
|
-
const timer = setTimeout(() =>
|
|
491
|
-
setDebouncedValue(value);
|
|
492
|
-
}, delay);
|
|
493
|
-
|
|
48
|
+
const timer = setTimeout(() => setDebouncedValue(value), delay);
|
|
494
49
|
return () => clearTimeout(timer);
|
|
495
50
|
}, [value, delay]);
|
|
496
51
|
|
|
497
52
|
return debouncedValue;
|
|
498
53
|
}
|
|
499
54
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
callback: T,
|
|
503
|
-
delay: number
|
|
504
|
-
): (...args: Parameters<T>) => void {
|
|
505
|
-
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
506
|
-
const callbackRef = useRef(callback);
|
|
507
|
-
|
|
508
|
-
// Keep callback ref updated
|
|
509
|
-
useEffect(() => {
|
|
510
|
-
callbackRef.current = callback;
|
|
511
|
-
}, [callback]);
|
|
512
|
-
|
|
513
|
-
// Cleanup on unmount
|
|
514
|
-
useEffect(() => {
|
|
515
|
-
return () => {
|
|
516
|
-
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
517
|
-
};
|
|
518
|
-
}, []);
|
|
519
|
-
|
|
520
|
-
return useCallback(
|
|
521
|
-
(...args: Parameters<T>) => {
|
|
522
|
-
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
523
|
-
|
|
524
|
-
timeoutRef.current = setTimeout(() => {
|
|
525
|
-
callbackRef.current(...args);
|
|
526
|
-
}, delay);
|
|
527
|
-
},
|
|
528
|
-
[delay]
|
|
529
|
-
);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
// hooks/useAsync.ts
|
|
533
|
-
interface AsyncState<T> {
|
|
534
|
-
data: T | null;
|
|
535
|
-
loading: boolean;
|
|
536
|
-
error: Error | null;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
interface UseAsyncReturn<T> extends AsyncState<T> {
|
|
540
|
-
execute: () => Promise<T | null>;
|
|
541
|
-
reset: () => void;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
export function useAsync<T>(
|
|
545
|
-
asyncFunction: () => Promise<T>,
|
|
546
|
-
immediate = true
|
|
547
|
-
): UseAsyncReturn<T> {
|
|
548
|
-
const [state, setState] = useState<AsyncState<T>>({
|
|
549
|
-
data: null,
|
|
550
|
-
loading: immediate,
|
|
551
|
-
error: null,
|
|
552
|
-
});
|
|
553
|
-
|
|
554
|
-
const execute = useCallback(async () => {
|
|
555
|
-
setState((prev) => ({ ...prev, loading: true, error: null }));
|
|
556
|
-
|
|
55
|
+
export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
56
|
+
const [stored, setStored] = useState<T>(() => {
|
|
557
57
|
try {
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
} catch (error) {
|
|
562
|
-
setState({
|
|
563
|
-
data: null,
|
|
564
|
-
loading: false,
|
|
565
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
566
|
-
});
|
|
567
|
-
return null;
|
|
568
|
-
}
|
|
569
|
-
}, [asyncFunction]);
|
|
570
|
-
|
|
571
|
-
const reset = useCallback(() => {
|
|
572
|
-
setState({ data: null, loading: false, error: null });
|
|
573
|
-
}, []);
|
|
574
|
-
|
|
575
|
-
useEffect(() => {
|
|
576
|
-
if (immediate) {
|
|
577
|
-
execute();
|
|
578
|
-
}
|
|
579
|
-
}, [execute, immediate]);
|
|
580
|
-
|
|
581
|
-
return { ...state, execute, reset };
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// hooks/useMediaQuery.ts
|
|
585
|
-
export function useMediaQuery(query: string): boolean {
|
|
586
|
-
const [matches, setMatches] = useState(() => {
|
|
587
|
-
if (typeof window === 'undefined') return false;
|
|
588
|
-
return window.matchMedia(query).matches;
|
|
58
|
+
const item = window.localStorage.getItem(key);
|
|
59
|
+
return item ? JSON.parse(item) : initialValue;
|
|
60
|
+
} catch { return initialValue; }
|
|
589
61
|
});
|
|
590
62
|
|
|
591
63
|
useEffect(() => {
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
// Set initial value
|
|
596
|
-
setMatches(mediaQuery.matches);
|
|
597
|
-
|
|
598
|
-
// Modern browsers
|
|
599
|
-
mediaQuery.addEventListener('change', handler);
|
|
600
|
-
return () => mediaQuery.removeEventListener('change', handler);
|
|
601
|
-
}, [query]);
|
|
602
|
-
|
|
603
|
-
return matches;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
// hooks/useClickOutside.ts
|
|
607
|
-
export function useClickOutside<T extends HTMLElement>(
|
|
608
|
-
callback: () => void
|
|
609
|
-
): React.RefObject<T> {
|
|
610
|
-
const ref = useRef<T>(null);
|
|
611
|
-
|
|
612
|
-
useEffect(() => {
|
|
613
|
-
const handleClick = (event: MouseEvent) => {
|
|
614
|
-
if (ref.current && !ref.current.contains(event.target as Node)) {
|
|
615
|
-
callback();
|
|
616
|
-
}
|
|
617
|
-
};
|
|
618
|
-
|
|
619
|
-
document.addEventListener('mousedown', handleClick);
|
|
620
|
-
return () => document.removeEventListener('mousedown', handleClick);
|
|
621
|
-
}, [callback]);
|
|
64
|
+
window.localStorage.setItem(key, JSON.stringify(stored));
|
|
65
|
+
}, [key, stored]);
|
|
622
66
|
|
|
623
|
-
return
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
// hooks/usePrevious.ts
|
|
627
|
-
export function usePrevious<T>(value: T): T | undefined {
|
|
628
|
-
const ref = useRef<T>();
|
|
629
|
-
|
|
630
|
-
useEffect(() => {
|
|
631
|
-
ref.current = value;
|
|
632
|
-
}, [value]);
|
|
633
|
-
|
|
634
|
-
return ref.current;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
// Usage examples
|
|
638
|
-
function HooksExamples() {
|
|
639
|
-
const [theme, setTheme] = useLocalStorage('theme', 'light');
|
|
640
|
-
const [search, setSearch] = useState('');
|
|
641
|
-
const debouncedSearch = useDebounce(search, 300);
|
|
642
|
-
const isMobile = useMediaQuery('(max-width: 768px)');
|
|
643
|
-
const { data, loading, error } = useAsync(() => fetchUsers());
|
|
644
|
-
|
|
645
|
-
const dropdownRef = useClickOutside<HTMLDivElement>(() => {
|
|
646
|
-
setIsOpen(false);
|
|
647
|
-
});
|
|
648
|
-
|
|
649
|
-
return (
|
|
650
|
-
<div>
|
|
651
|
-
<ThemeToggle theme={theme} onChange={setTheme} />
|
|
652
|
-
<SearchInput value={search} onChange={setSearch} />
|
|
653
|
-
<SearchResults query={debouncedSearch} />
|
|
654
|
-
{isMobile ? <MobileNav /> : <DesktopNav />}
|
|
655
|
-
</div>
|
|
656
|
-
);
|
|
67
|
+
return [stored, setStored] as const;
|
|
657
68
|
}
|
|
658
69
|
```
|
|
659
70
|
|
|
660
|
-
###
|
|
71
|
+
### State Machine Pattern
|
|
661
72
|
|
|
662
73
|
```tsx
|
|
663
|
-
// lib/createStateMachine.ts
|
|
664
|
-
type StateConfig<TContext, TState extends string, TEvent extends { type: string }> = {
|
|
665
|
-
initial: TState;
|
|
666
|
-
context: TContext;
|
|
667
|
-
states: {
|
|
668
|
-
[K in TState]: {
|
|
669
|
-
on?: {
|
|
670
|
-
[E in TEvent['type']]?: TState | {
|
|
671
|
-
target: TState;
|
|
672
|
-
actions?: (context: TContext, event: Extract<TEvent, { type: E }>) => TContext;
|
|
673
|
-
guard?: (context: TContext, event: Extract<TEvent, { type: E }>) => boolean;
|
|
674
|
-
};
|
|
675
|
-
};
|
|
676
|
-
entry?: (context: TContext) => TContext | void;
|
|
677
|
-
exit?: (context: TContext) => TContext | void;
|
|
678
|
-
};
|
|
679
|
-
};
|
|
680
|
-
};
|
|
681
|
-
|
|
682
|
-
// hooks/useStateMachine.ts
|
|
683
|
-
export function useStateMachine<
|
|
684
|
-
TContext,
|
|
685
|
-
TState extends string,
|
|
686
|
-
TEvent extends { type: string }
|
|
687
|
-
>(config: StateConfig<TContext, TState, TEvent>) {
|
|
688
|
-
const [state, setState] = useState<TState>(config.initial);
|
|
689
|
-
const [context, setContext] = useState<TContext>(config.context);
|
|
690
|
-
|
|
691
|
-
const send = useCallback(
|
|
692
|
-
(event: TEvent) => {
|
|
693
|
-
const currentStateConfig = config.states[state];
|
|
694
|
-
const transition = currentStateConfig.on?.[event.type as TEvent['type']];
|
|
695
|
-
|
|
696
|
-
if (!transition) return;
|
|
697
|
-
|
|
698
|
-
let nextState: TState;
|
|
699
|
-
let newContext = context;
|
|
700
|
-
|
|
701
|
-
if (typeof transition === 'string') {
|
|
702
|
-
nextState = transition;
|
|
703
|
-
} else {
|
|
704
|
-
// Check guard condition
|
|
705
|
-
if (transition.guard && !transition.guard(context, event as any)) {
|
|
706
|
-
return;
|
|
707
|
-
}
|
|
708
|
-
nextState = transition.target;
|
|
709
|
-
if (transition.actions) {
|
|
710
|
-
newContext = transition.actions(context, event as any);
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
// Run exit action
|
|
715
|
-
const exitAction = currentStateConfig.exit;
|
|
716
|
-
if (exitAction) {
|
|
717
|
-
const result = exitAction(newContext);
|
|
718
|
-
if (result) newContext = result;
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
// Run entry action
|
|
722
|
-
const nextStateConfig = config.states[nextState];
|
|
723
|
-
const entryAction = nextStateConfig.entry;
|
|
724
|
-
if (entryAction) {
|
|
725
|
-
const result = entryAction(newContext);
|
|
726
|
-
if (result) newContext = result;
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
setState(nextState);
|
|
730
|
-
setContext(newContext);
|
|
731
|
-
},
|
|
732
|
-
[state, context, config]
|
|
733
|
-
);
|
|
734
|
-
|
|
735
|
-
const matches = useCallback((s: TState) => state === s, [state]);
|
|
736
|
-
|
|
737
|
-
return { state, context, send, matches };
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
// Example: Form submission state machine
|
|
741
74
|
type FormState = 'idle' | 'validating' | 'submitting' | 'success' | 'error';
|
|
742
|
-
|
|
743
75
|
type FormEvent =
|
|
744
76
|
| { type: 'SUBMIT'; data: FormData }
|
|
745
|
-
| { type: '
|
|
746
|
-
| { type: '
|
|
747
|
-
| { type: 'SUBMIT_SUCCESS'; response: any }
|
|
748
|
-
| { type: 'SUBMIT_ERROR'; error: string }
|
|
749
|
-
| { type: 'RESET' };
|
|
77
|
+
| { type: 'SUCCESS'; response: any }
|
|
78
|
+
| { type: 'ERROR'; error: string };
|
|
750
79
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
response: any;
|
|
755
|
-
submitError: string | null;
|
|
756
|
-
}
|
|
80
|
+
function useFormMachine() {
|
|
81
|
+
const [state, setState] = useState<FormState>('idle');
|
|
82
|
+
const [context, setContext] = useState({ data: null, error: null });
|
|
757
83
|
|
|
758
|
-
const
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
idle: {
|
|
768
|
-
on: {
|
|
769
|
-
SUBMIT: {
|
|
770
|
-
target: 'validating',
|
|
771
|
-
actions: (ctx, event) => ({ ...ctx, data: event.data, errors: {} }),
|
|
772
|
-
},
|
|
773
|
-
},
|
|
774
|
-
},
|
|
775
|
-
validating: {
|
|
776
|
-
on: {
|
|
777
|
-
VALIDATE_SUCCESS: 'submitting',
|
|
778
|
-
VALIDATE_ERROR: {
|
|
779
|
-
target: 'idle',
|
|
780
|
-
actions: (ctx, event) => ({ ...ctx, errors: event.errors }),
|
|
781
|
-
},
|
|
782
|
-
},
|
|
783
|
-
},
|
|
784
|
-
submitting: {
|
|
785
|
-
on: {
|
|
786
|
-
SUBMIT_SUCCESS: {
|
|
787
|
-
target: 'success',
|
|
788
|
-
actions: (ctx, event) => ({ ...ctx, response: event.response }),
|
|
789
|
-
},
|
|
790
|
-
SUBMIT_ERROR: {
|
|
791
|
-
target: 'error',
|
|
792
|
-
actions: (ctx, event) => ({ ...ctx, submitError: event.error }),
|
|
793
|
-
},
|
|
794
|
-
},
|
|
795
|
-
},
|
|
796
|
-
success: {
|
|
797
|
-
on: {
|
|
798
|
-
RESET: {
|
|
799
|
-
target: 'idle',
|
|
800
|
-
actions: () => ({
|
|
801
|
-
data: null,
|
|
802
|
-
errors: {},
|
|
803
|
-
response: null,
|
|
804
|
-
submitError: null,
|
|
805
|
-
}),
|
|
806
|
-
},
|
|
807
|
-
},
|
|
808
|
-
},
|
|
809
|
-
error: {
|
|
810
|
-
on: {
|
|
811
|
-
SUBMIT: {
|
|
812
|
-
target: 'validating',
|
|
813
|
-
actions: (ctx, event) => ({
|
|
814
|
-
...ctx,
|
|
815
|
-
data: event.data,
|
|
816
|
-
submitError: null,
|
|
817
|
-
}),
|
|
818
|
-
},
|
|
819
|
-
RESET: {
|
|
820
|
-
target: 'idle',
|
|
821
|
-
actions: () => ({
|
|
822
|
-
data: null,
|
|
823
|
-
errors: {},
|
|
824
|
-
response: null,
|
|
825
|
-
submitError: null,
|
|
826
|
-
}),
|
|
827
|
-
},
|
|
828
|
-
},
|
|
829
|
-
},
|
|
830
|
-
},
|
|
831
|
-
};
|
|
832
|
-
|
|
833
|
-
function FormWithStateMachine() {
|
|
834
|
-
const { state, context, send, matches } = useStateMachine(formMachine);
|
|
835
|
-
|
|
836
|
-
const handleSubmit = async (formData: FormData) => {
|
|
837
|
-
send({ type: 'SUBMIT', data: formData });
|
|
838
|
-
|
|
839
|
-
// Validate
|
|
840
|
-
const errors = validate(formData);
|
|
841
|
-
if (Object.keys(errors).length > 0) {
|
|
842
|
-
send({ type: 'VALIDATE_ERROR', errors });
|
|
843
|
-
return;
|
|
84
|
+
const send = useCallback((event: FormEvent) => {
|
|
85
|
+
switch (state) {
|
|
86
|
+
case 'idle':
|
|
87
|
+
if (event.type === 'SUBMIT') { setState('validating'); }
|
|
88
|
+
break;
|
|
89
|
+
case 'submitting':
|
|
90
|
+
if (event.type === 'SUCCESS') { setState('success'); }
|
|
91
|
+
if (event.type === 'ERROR') { setState('error'); setContext(c => ({ ...c, error: event.error })); }
|
|
92
|
+
break;
|
|
844
93
|
}
|
|
845
|
-
|
|
94
|
+
}, [state]);
|
|
846
95
|
|
|
847
|
-
|
|
848
|
-
try {
|
|
849
|
-
const response = await submitForm(formData);
|
|
850
|
-
send({ type: 'SUBMIT_SUCCESS', response });
|
|
851
|
-
} catch (error) {
|
|
852
|
-
send({ type: 'SUBMIT_ERROR', error: error.message });
|
|
853
|
-
}
|
|
854
|
-
};
|
|
855
|
-
|
|
856
|
-
return (
|
|
857
|
-
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(new FormData(e.target)); }}>
|
|
858
|
-
{matches('success') && <SuccessMessage response={context.response} />}
|
|
859
|
-
{matches('error') && <ErrorMessage error={context.submitError} />}
|
|
860
|
-
{Object.entries(context.errors).map(([field, error]) => (
|
|
861
|
-
<FieldError key={field} field={field} error={error} />
|
|
862
|
-
))}
|
|
863
|
-
<button type="submit" disabled={matches('submitting') || matches('validating')}>
|
|
864
|
-
{matches('submitting') ? 'Submitting...' : 'Submit'}
|
|
865
|
-
</button>
|
|
866
|
-
</form>
|
|
867
|
-
);
|
|
96
|
+
return { state, context, send };
|
|
868
97
|
}
|
|
869
98
|
```
|
|
870
99
|
|
|
871
|
-
###
|
|
100
|
+
### Optimistic Update Hook
|
|
872
101
|
|
|
873
102
|
```tsx
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
import { useAuth } from '@/hooks/useAuth';
|
|
878
|
-
|
|
879
|
-
interface WithAuthOptions {
|
|
880
|
-
redirectTo?: string;
|
|
881
|
-
requiredRole?: string;
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
export function withAuth<P extends object>(
|
|
885
|
-
WrappedComponent: ComponentType<P>,
|
|
886
|
-
options: WithAuthOptions = {}
|
|
887
|
-
) {
|
|
888
|
-
const { redirectTo = '/login', requiredRole } = options;
|
|
889
|
-
|
|
890
|
-
function AuthenticatedComponent(props: P) {
|
|
891
|
-
const router = useRouter();
|
|
892
|
-
const { user, loading, isAuthenticated } = useAuth();
|
|
893
|
-
|
|
894
|
-
useEffect(() => {
|
|
895
|
-
if (!loading && !isAuthenticated) {
|
|
896
|
-
router.push(`${redirectTo}?returnUrl=${encodeURIComponent(router.asPath)}`);
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
if (!loading && requiredRole && user?.role !== requiredRole) {
|
|
900
|
-
router.push('/unauthorized');
|
|
901
|
-
}
|
|
902
|
-
}, [loading, isAuthenticated, user, router]);
|
|
903
|
-
|
|
904
|
-
if (loading) {
|
|
905
|
-
return <LoadingSpinner />;
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
if (!isAuthenticated) {
|
|
909
|
-
return null;
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
if (requiredRole && user?.role !== requiredRole) {
|
|
913
|
-
return null;
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
return <WrappedComponent {...props} />;
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
AuthenticatedComponent.displayName = `withAuth(${
|
|
920
|
-
WrappedComponent.displayName || WrappedComponent.name || 'Component'
|
|
921
|
-
})`;
|
|
922
|
-
|
|
923
|
-
return AuthenticatedComponent;
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
// hocs/withErrorBoundary.tsx
|
|
927
|
-
interface WithErrorBoundaryOptions {
|
|
928
|
-
fallback?: ReactNode;
|
|
929
|
-
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
export function withErrorBoundary<P extends object>(
|
|
933
|
-
WrappedComponent: ComponentType<P>,
|
|
934
|
-
options: WithErrorBoundaryOptions = {}
|
|
935
|
-
) {
|
|
936
|
-
const { fallback, onError } = options;
|
|
937
|
-
|
|
938
|
-
class ErrorBoundaryHOC extends React.Component<
|
|
939
|
-
P,
|
|
940
|
-
{ hasError: boolean; error: Error | null }
|
|
941
|
-
> {
|
|
942
|
-
static displayName = `withErrorBoundary(${
|
|
943
|
-
WrappedComponent.displayName || WrappedComponent.name || 'Component'
|
|
944
|
-
})`;
|
|
945
|
-
|
|
946
|
-
constructor(props: P) {
|
|
947
|
-
super(props);
|
|
948
|
-
this.state = { hasError: false, error: null };
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
static getDerivedStateFromError(error: Error) {
|
|
952
|
-
return { hasError: true, error };
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
|
956
|
-
onError?.(error, errorInfo);
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
render() {
|
|
960
|
-
if (this.state.hasError) {
|
|
961
|
-
return fallback || <DefaultErrorFallback error={this.state.error} />;
|
|
962
|
-
}
|
|
103
|
+
export function useOptimistic<T>(initialData: T, reducer: (state: T, action: any) => T) {
|
|
104
|
+
const [state, setState] = useState({ data: initialData, pending: false, error: null });
|
|
105
|
+
const previousRef = useRef(initialData);
|
|
963
106
|
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
return ErrorBoundaryHOC;
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
// hocs/withLoading.tsx
|
|
972
|
-
interface WithLoadingProps {
|
|
973
|
-
isLoading?: boolean;
|
|
974
|
-
}
|
|
107
|
+
const optimisticUpdate = useCallback(async (action: any, asyncOp: () => Promise<T>) => {
|
|
108
|
+
previousRef.current = state.data;
|
|
109
|
+
setState({ data: reducer(state.data, action), pending: true, error: null });
|
|
975
110
|
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
) {
|
|
980
|
-
|
|
981
|
-
const { isLoading, ...rest } = props;
|
|
982
|
-
|
|
983
|
-
if (isLoading) {
|
|
984
|
-
return <LoadingComponent />;
|
|
111
|
+
try {
|
|
112
|
+
const result = await asyncOp();
|
|
113
|
+
setState({ data: result, pending: false, error: null });
|
|
114
|
+
} catch (error) {
|
|
115
|
+
setState({ data: previousRef.current, pending: false, error: error as Error });
|
|
985
116
|
}
|
|
986
|
-
|
|
987
|
-
return <WrappedComponent {...(rest as P)} />;
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
LoadingWrapper.displayName = `withLoading(${
|
|
991
|
-
WrappedComponent.displayName || WrappedComponent.name || 'Component'
|
|
992
|
-
})`;
|
|
993
|
-
|
|
994
|
-
return LoadingWrapper;
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
// Usage
|
|
998
|
-
const ProtectedDashboard = withAuth(Dashboard, { requiredRole: 'admin' });
|
|
999
|
-
const SafeUserProfile = withErrorBoundary(UserProfile, {
|
|
1000
|
-
fallback: <div>Something went wrong</div>,
|
|
1001
|
-
});
|
|
1002
|
-
const LoadableDataGrid = withLoading(DataGrid);
|
|
1003
|
-
```
|
|
1004
|
-
|
|
1005
|
-
### 6. Optimistic UI Updates
|
|
1006
|
-
|
|
1007
|
-
```tsx
|
|
1008
|
-
// hooks/useOptimistic.ts
|
|
1009
|
-
interface OptimisticState<T> {
|
|
1010
|
-
data: T;
|
|
1011
|
-
pending: boolean;
|
|
1012
|
-
error: Error | null;
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
export function useOptimistic<T, TAction>(
|
|
1016
|
-
initialData: T,
|
|
1017
|
-
reducer: (state: T, action: TAction) => T
|
|
1018
|
-
) {
|
|
1019
|
-
const [state, setState] = useState<OptimisticState<T>>({
|
|
1020
|
-
data: initialData,
|
|
1021
|
-
pending: false,
|
|
1022
|
-
error: null,
|
|
1023
|
-
});
|
|
1024
|
-
|
|
1025
|
-
const previousDataRef = useRef<T>(initialData);
|
|
1026
|
-
|
|
1027
|
-
const optimisticUpdate = useCallback(
|
|
1028
|
-
async (action: TAction, asyncOperation: () => Promise<T>) => {
|
|
1029
|
-
// Store previous state for rollback
|
|
1030
|
-
previousDataRef.current = state.data;
|
|
1031
|
-
|
|
1032
|
-
// Apply optimistic update
|
|
1033
|
-
const optimisticData = reducer(state.data, action);
|
|
1034
|
-
setState({ data: optimisticData, pending: true, error: null });
|
|
1035
|
-
|
|
1036
|
-
try {
|
|
1037
|
-
// Perform actual async operation
|
|
1038
|
-
const result = await asyncOperation();
|
|
1039
|
-
setState({ data: result, pending: false, error: null });
|
|
1040
|
-
return result;
|
|
1041
|
-
} catch (error) {
|
|
1042
|
-
// Rollback on error
|
|
1043
|
-
setState({
|
|
1044
|
-
data: previousDataRef.current,
|
|
1045
|
-
pending: false,
|
|
1046
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
1047
|
-
});
|
|
1048
|
-
throw error;
|
|
1049
|
-
}
|
|
1050
|
-
},
|
|
1051
|
-
[state.data, reducer]
|
|
1052
|
-
);
|
|
117
|
+
}, [state.data, reducer]);
|
|
1053
118
|
|
|
1054
119
|
return { ...state, optimisticUpdate };
|
|
1055
120
|
}
|
|
1056
|
-
|
|
1057
|
-
// Example: Todo list with optimistic updates
|
|
1058
|
-
interface Todo {
|
|
1059
|
-
id: string;
|
|
1060
|
-
text: string;
|
|
1061
|
-
completed: boolean;
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
function TodoList() {
|
|
1065
|
-
const { data: todos, pending, error, optimisticUpdate } = useOptimistic<
|
|
1066
|
-
Todo[],
|
|
1067
|
-
{ type: 'ADD' | 'TOGGLE' | 'DELETE'; payload: any }
|
|
1068
|
-
>(initialTodos, (state, action) => {
|
|
1069
|
-
switch (action.type) {
|
|
1070
|
-
case 'ADD':
|
|
1071
|
-
return [...state, action.payload];
|
|
1072
|
-
case 'TOGGLE':
|
|
1073
|
-
return state.map((todo) =>
|
|
1074
|
-
todo.id === action.payload
|
|
1075
|
-
? { ...todo, completed: !todo.completed }
|
|
1076
|
-
: todo
|
|
1077
|
-
);
|
|
1078
|
-
case 'DELETE':
|
|
1079
|
-
return state.filter((todo) => todo.id !== action.payload);
|
|
1080
|
-
default:
|
|
1081
|
-
return state;
|
|
1082
|
-
}
|
|
1083
|
-
});
|
|
1084
|
-
|
|
1085
|
-
const addTodo = async (text: string) => {
|
|
1086
|
-
const tempId = `temp-${Date.now()}`;
|
|
1087
|
-
const newTodo = { id: tempId, text, completed: false };
|
|
1088
|
-
|
|
1089
|
-
await optimisticUpdate(
|
|
1090
|
-
{ type: 'ADD', payload: newTodo },
|
|
1091
|
-
async () => {
|
|
1092
|
-
const response = await api.createTodo({ text });
|
|
1093
|
-
return todos.map((t) => (t.id === tempId ? response : t));
|
|
1094
|
-
}
|
|
1095
|
-
);
|
|
1096
|
-
};
|
|
1097
|
-
|
|
1098
|
-
const toggleTodo = async (id: string) => {
|
|
1099
|
-
await optimisticUpdate(
|
|
1100
|
-
{ type: 'TOGGLE', payload: id },
|
|
1101
|
-
async () => {
|
|
1102
|
-
await api.toggleTodo(id);
|
|
1103
|
-
return todos.map((t) =>
|
|
1104
|
-
t.id === id ? { ...t, completed: !t.completed } : t
|
|
1105
|
-
);
|
|
1106
|
-
}
|
|
1107
|
-
);
|
|
1108
|
-
};
|
|
1109
|
-
|
|
1110
|
-
return (
|
|
1111
|
-
<div className={pending ? 'opacity-70' : ''}>
|
|
1112
|
-
{error && <ErrorToast message={error.message} />}
|
|
1113
|
-
<TodoForm onSubmit={addTodo} disabled={pending} />
|
|
1114
|
-
{todos.map((todo) => (
|
|
1115
|
-
<TodoItem
|
|
1116
|
-
key={todo.id}
|
|
1117
|
-
todo={todo}
|
|
1118
|
-
onToggle={() => toggleTodo(todo.id)}
|
|
1119
|
-
/>
|
|
1120
|
-
))}
|
|
1121
|
-
</div>
|
|
1122
|
-
);
|
|
1123
|
-
}
|
|
1124
|
-
```
|
|
1125
|
-
|
|
1126
|
-
## Use Cases
|
|
1127
|
-
|
|
1128
|
-
### Component Library Architecture
|
|
1129
|
-
|
|
1130
|
-
```tsx
|
|
1131
|
-
// lib/components/index.ts - Barrel exports
|
|
1132
|
-
export { Button, type ButtonProps } from './Button';
|
|
1133
|
-
export { Input, type InputProps } from './Input';
|
|
1134
|
-
export { Select, SelectTrigger, SelectContent, SelectItem } from './Select';
|
|
1135
|
-
|
|
1136
|
-
// lib/components/Button/Button.tsx
|
|
1137
|
-
import { forwardRef, ButtonHTMLAttributes } from 'react';
|
|
1138
|
-
import { cva, type VariantProps } from 'class-variance-authority';
|
|
1139
|
-
import { cn } from '@/lib/utils';
|
|
1140
|
-
|
|
1141
|
-
const buttonVariants = cva(
|
|
1142
|
-
'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50',
|
|
1143
|
-
{
|
|
1144
|
-
variants: {
|
|
1145
|
-
variant: {
|
|
1146
|
-
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
1147
|
-
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
|
1148
|
-
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
|
1149
|
-
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
1150
|
-
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
|
1151
|
-
link: 'text-primary underline-offset-4 hover:underline',
|
|
1152
|
-
},
|
|
1153
|
-
size: {
|
|
1154
|
-
default: 'h-10 px-4 py-2',
|
|
1155
|
-
sm: 'h-9 rounded-md px-3',
|
|
1156
|
-
lg: 'h-11 rounded-md px-8',
|
|
1157
|
-
icon: 'h-10 w-10',
|
|
1158
|
-
},
|
|
1159
|
-
},
|
|
1160
|
-
defaultVariants: {
|
|
1161
|
-
variant: 'default',
|
|
1162
|
-
size: 'default',
|
|
1163
|
-
},
|
|
1164
|
-
}
|
|
1165
|
-
);
|
|
1166
|
-
|
|
1167
|
-
export interface ButtonProps
|
|
1168
|
-
extends ButtonHTMLAttributes<HTMLButtonElement>,
|
|
1169
|
-
VariantProps<typeof buttonVariants> {
|
|
1170
|
-
loading?: boolean;
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
1174
|
-
({ className, variant, size, loading, children, disabled, ...props }, ref) => {
|
|
1175
|
-
return (
|
|
1176
|
-
<button
|
|
1177
|
-
className={cn(buttonVariants({ variant, size, className }))}
|
|
1178
|
-
ref={ref}
|
|
1179
|
-
disabled={disabled || loading}
|
|
1180
|
-
{...props}
|
|
1181
|
-
>
|
|
1182
|
-
{loading && <Spinner className="mr-2 h-4 w-4" />}
|
|
1183
|
-
{children}
|
|
1184
|
-
</button>
|
|
1185
|
-
);
|
|
1186
|
-
}
|
|
1187
|
-
);
|
|
1188
|
-
|
|
1189
|
-
Button.displayName = 'Button';
|
|
1190
|
-
```
|
|
1191
|
-
|
|
1192
|
-
### Form Architecture Pattern
|
|
1193
|
-
|
|
1194
|
-
```tsx
|
|
1195
|
-
// hooks/useForm.ts
|
|
1196
|
-
interface UseFormConfig<T> {
|
|
1197
|
-
initialValues: T;
|
|
1198
|
-
validate: (values: T) => Partial<Record<keyof T, string>>;
|
|
1199
|
-
onSubmit: (values: T) => Promise<void>;
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
export function useForm<T extends Record<string, any>>({
|
|
1203
|
-
initialValues,
|
|
1204
|
-
validate,
|
|
1205
|
-
onSubmit,
|
|
1206
|
-
}: UseFormConfig<T>) {
|
|
1207
|
-
const [values, setValues] = useState<T>(initialValues);
|
|
1208
|
-
const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
|
|
1209
|
-
const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({});
|
|
1210
|
-
const [submitting, setSubmitting] = useState(false);
|
|
1211
|
-
|
|
1212
|
-
const handleChange = useCallback(
|
|
1213
|
-
(field: keyof T) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
1214
|
-
const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
|
|
1215
|
-
setValues((prev) => ({ ...prev, [field]: value }));
|
|
1216
|
-
},
|
|
1217
|
-
[]
|
|
1218
|
-
);
|
|
1219
|
-
|
|
1220
|
-
const handleBlur = useCallback(
|
|
1221
|
-
(field: keyof T) => () => {
|
|
1222
|
-
setTouched((prev) => ({ ...prev, [field]: true }));
|
|
1223
|
-
const fieldErrors = validate(values);
|
|
1224
|
-
if (fieldErrors[field]) {
|
|
1225
|
-
setErrors((prev) => ({ ...prev, [field]: fieldErrors[field] }));
|
|
1226
|
-
} else {
|
|
1227
|
-
setErrors((prev) => {
|
|
1228
|
-
const { [field]: _, ...rest } = prev;
|
|
1229
|
-
return rest as Partial<Record<keyof T, string>>;
|
|
1230
|
-
});
|
|
1231
|
-
}
|
|
1232
|
-
},
|
|
1233
|
-
[values, validate]
|
|
1234
|
-
);
|
|
1235
|
-
|
|
1236
|
-
const handleSubmit = useCallback(
|
|
1237
|
-
async (e: React.FormEvent) => {
|
|
1238
|
-
e.preventDefault();
|
|
1239
|
-
|
|
1240
|
-
const validationErrors = validate(values);
|
|
1241
|
-
setErrors(validationErrors);
|
|
1242
|
-
setTouched(
|
|
1243
|
-
Object.keys(values).reduce((acc, key) => ({ ...acc, [key]: true }), {})
|
|
1244
|
-
);
|
|
1245
|
-
|
|
1246
|
-
if (Object.keys(validationErrors).length > 0) return;
|
|
1247
|
-
|
|
1248
|
-
setSubmitting(true);
|
|
1249
|
-
try {
|
|
1250
|
-
await onSubmit(values);
|
|
1251
|
-
} finally {
|
|
1252
|
-
setSubmitting(false);
|
|
1253
|
-
}
|
|
1254
|
-
},
|
|
1255
|
-
[values, validate, onSubmit]
|
|
1256
|
-
);
|
|
1257
|
-
|
|
1258
|
-
const reset = useCallback(() => {
|
|
1259
|
-
setValues(initialValues);
|
|
1260
|
-
setErrors({});
|
|
1261
|
-
setTouched({});
|
|
1262
|
-
}, [initialValues]);
|
|
1263
|
-
|
|
1264
|
-
const getFieldProps = useCallback(
|
|
1265
|
-
(field: keyof T) => ({
|
|
1266
|
-
value: values[field],
|
|
1267
|
-
onChange: handleChange(field),
|
|
1268
|
-
onBlur: handleBlur(field),
|
|
1269
|
-
error: touched[field] ? errors[field] : undefined,
|
|
1270
|
-
}),
|
|
1271
|
-
[values, errors, touched, handleChange, handleBlur]
|
|
1272
|
-
);
|
|
1273
|
-
|
|
1274
|
-
return {
|
|
1275
|
-
values,
|
|
1276
|
-
errors,
|
|
1277
|
-
touched,
|
|
1278
|
-
submitting,
|
|
1279
|
-
handleChange,
|
|
1280
|
-
handleBlur,
|
|
1281
|
-
handleSubmit,
|
|
1282
|
-
reset,
|
|
1283
|
-
getFieldProps,
|
|
1284
|
-
setValues,
|
|
1285
|
-
setFieldValue: (field: keyof T, value: T[keyof T]) =>
|
|
1286
|
-
setValues((prev) => ({ ...prev, [field]: value })),
|
|
1287
|
-
};
|
|
1288
|
-
}
|
|
1289
121
|
```
|
|
1290
122
|
|
|
1291
123
|
## Best Practices
|
|
1292
124
|
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
- Colocate related code and styles
|
|
1302
|
-
- Use forwardRef for component library primitives
|
|
1303
|
-
- Implement proper error boundaries
|
|
1304
|
-
- Use render props for maximum flexibility
|
|
1305
|
-
|
|
1306
|
-
### Don'ts
|
|
1307
|
-
|
|
1308
|
-
- Don't overuse HOCs (prefer hooks)
|
|
1309
|
-
- Don't mutate state directly
|
|
1310
|
-
- Don't create deeply nested component hierarchies
|
|
1311
|
-
- Don't pass too many props (use context or composition)
|
|
1312
|
-
- Don't ignore TypeScript errors
|
|
1313
|
-
- Don't create components with side effects in render
|
|
1314
|
-
- Don't forget to memoize expensive computations
|
|
1315
|
-
- Don't skip accessibility in component design
|
|
1316
|
-
- Don't use prop drilling for deeply nested data
|
|
1317
|
-
- Don't forget cleanup in useEffect
|
|
1318
|
-
|
|
1319
|
-
## References
|
|
1320
|
-
|
|
1321
|
-
- [React Patterns](https://reactpatterns.com/)
|
|
1322
|
-
- [Patterns.dev](https://www.patterns.dev/)
|
|
1323
|
-
- [Kent C. Dodds - Advanced React Patterns](https://kentcdodds.com/blog/advanced-react-patterns)
|
|
1324
|
-
- [XState Documentation](https://xstate.js.org/docs/)
|
|
1325
|
-
- [React TypeScript Cheatsheet](https://react-typescript-cheatsheet.netlify.app/)
|
|
125
|
+
| Do | Avoid |
|
|
126
|
+
|----|-------|
|
|
127
|
+
| Use compound components for complex UI with shared state | Overusing HOCs (prefer hooks) |
|
|
128
|
+
| Create custom hooks to encapsulate reusable logic | Mutating state directly |
|
|
129
|
+
| Implement state machines for complex state transitions | Deeply nested component hierarchies |
|
|
130
|
+
| Use TypeScript for type-safe component APIs | Passing too many props (use context/composition) |
|
|
131
|
+
| Use forwardRef for component library primitives | Creating components with side effects in render |
|
|
132
|
+
| Keep components focused with single responsibility | Prop drilling for deeply nested data |
|