vibes-plug 1.0.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/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- package/BLUEPRINT.md +125 -0
- package/CHANGELOG.md +195 -0
- package/CONTRIBUTING.md +199 -0
- package/LICENSE +21 -0
- package/README.md +263 -0
- package/SECURITY.md +21 -0
- package/banner.png +0 -0
- package/package.json +25 -0
- package/plugin.json +8 -0
- package/scripts/update_skills.js +75 -0
- package/skills/ai-llm-integration-expert/SKILL.md +162 -0
- package/skills/api-design-expert/SKILL.md +310 -0
- package/skills/app-analyzer-optimizer/SKILL.md +189 -0
- package/skills/asisten_ramah/SKILL.md +41 -0
- package/skills/authentication-identity-expert/SKILL.md +45 -0
- package/skills/auto-doc-updater/SKILL.md +204 -0
- package/skills/bootstrap-to-modern/SKILL.md +87 -0
- package/skills/brainstorming/SKILL.md +353 -0
- package/skills/bun-runtime-expert/SKILL.md +211 -0
- package/skills/ci-cd-devops-architect/SKILL.md +45 -0
- package/skills/cloud-hosting-expert/SKILL.md +244 -0
- package/skills/coderabbit/SKILL.md +192 -0
- package/skills/data-telemetry-expert/SKILL.md +213 -0
- package/skills/database-orm-expert/SKILL.md +294 -0
- package/skills/design-system-architect/SKILL.md +243 -0
- package/skills/e2e-testing-expert/SKILL.md +315 -0
- package/skills/event-driven-architect/SKILL.md +81 -0
- package/skills/firebase-security-expert/SKILL.md +195 -0
- package/skills/fullstack-expert/SKILL.md +202 -0
- package/skills/fullstack-expert/references/api_design_guide.md +466 -0
- package/skills/fullstack-expert/references/devops_infrastructure.md +477 -0
- package/skills/fullstack-expert/references/multi_language_backend.md +528 -0
- package/skills/fullstack-expert/references/system_design_patterns.md +358 -0
- package/skills/fullstack-expert/scripts/api_contract_validator.py +253 -0
- package/skills/fullstack-expert/scripts/architecture_analyzer.py +326 -0
- package/skills/gemini-agent-booster/SKILL.md +135 -0
- package/skills/global-a11y-i18n-expert/SKILL.md +81 -0
- package/skills/go-programming-expert/SKILL.md +295 -0
- package/skills/hig/SKILL.md +188 -0
- package/skills/js-backend-expert/SKILL.md +192 -0
- package/skills/mcp-server-architect/SKILL.md +194 -0
- package/skills/mobile-expo-expert/SKILL.md +186 -0
- package/skills/monday-design-aesthetic/SKILL.md +67 -0
- package/skills/monorepo-architect/SKILL.md +227 -0
- package/skills/mpa-orchestrator/SKILL.md +101 -0
- package/skills/multi-agent-orchestration/SKILL.md +234 -0
- package/skills/multiple-entry-points/SKILL.md +55 -0
- package/skills/mvc-expert/SKILL.md +231 -0
- package/skills/payment-gateway-expert/SKILL.md +45 -0
- package/skills/performance-web-vitals/SKILL.md +332 -0
- package/skills/prd-architect/SKILL.md +191 -0
- package/skills/production-ready-hardener/SKILL.md +469 -0
- package/skills/production-ready-hardener/references/performance_optimization.md +441 -0
- package/skills/production-ready-hardener/references/production_checklist.md +161 -0
- package/skills/production-ready-hardener/references/security_hardening_guide.md +379 -0
- package/skills/production-ready-hardener/scripts/production_readiness_scanner.py +875 -0
- package/skills/python-programming-expert/SKILL.md +271 -0
- package/skills/realtime-collaboration-expert/SKILL.md +45 -0
- package/skills/rust-programming-expert/SKILL.md +235 -0
- package/skills/saas-billing/SKILL.md +377 -0
- package/skills/saas-multi-tenant/SKILL.md +237 -0
- package/skills/saas-mvp-launcher/SKILL.md +231 -0
- package/skills/saas-transformer/SKILL.md +446 -0
- package/skills/saas-transformer/references/billing_integration_guide.md +401 -0
- package/skills/saas-transformer/references/feature_gating_patterns.md +137 -0
- package/skills/saas-transformer/references/saas_transformation_checklist.md +121 -0
- package/skills/saas-transformer/scripts/saas_transformation_scanner.py +254 -0
- package/skills/scalability-clean-code/SKILL.md +229 -0
- package/skills/secure-fuzz-testing/SKILL.md +201 -0
- package/skills/senior-frontend/SKILL.md +161 -0
- package/skills/senior-frontend/references/frontend_best_practices.md +806 -0
- package/skills/senior-frontend/references/nextjs_optimization_guide.md +724 -0
- package/skills/senior-frontend/references/react_patterns.md +746 -0
- package/skills/senior-frontend/scripts/bundle_analyzer.py +407 -0
- package/skills/senior-frontend/scripts/component_generator.py +329 -0
- package/skills/senior-frontend/scripts/frontend_scaffolder.py +1005 -0
- package/skills/senior-fullstack/SKILL.md +167 -0
- package/skills/senior-fullstack/references/architecture_patterns.md +160 -0
- package/skills/senior-fullstack/references/development_workflows.md +222 -0
- package/skills/senior-fullstack/references/tech_stack_guide.md +190 -0
- package/skills/senior-fullstack/scripts/code_quality_analyzer.py +114 -0
- package/skills/senior-fullstack/scripts/fullstack_scaffolder.py +114 -0
- package/skills/senior-fullstack/scripts/project_scaffolder.py +114 -0
- package/skills/seo/SKILL.md +225 -0
- package/skills/seo/references/cwv-thresholds.md +108 -0
- package/skills/seo/references/eeat-framework.md +214 -0
- package/skills/seo/references/quality-gates.md +155 -0
- package/skills/seo/references/schema-types.md +118 -0
- package/skills/seo-aeo-landing-page-writer/SKILL.md +97 -0
- package/skills/seo-geo/SKILL.md +188 -0
- package/skills/session-handoff-resume/SKILL.md +158 -0
- package/skills/skill_baru/SKILL.md +147 -0
- package/skills/spa-orchestrator/SKILL.md +288 -0
- package/skills/state-management-expert/SKILL.md +272 -0
- package/skills/supabase-migration/SKILL.md +45 -0
- package/skills/supabase-security-expert/SKILL.md +243 -0
- package/skills/tailwind-expert/SKILL.md +188 -0
- package/skills/tanstack-query-expert/SKILL.md +199 -0
- package/skills/tauri-expert/SKILL.md +97 -0
- package/skills/token-saver/SKILL.md +111 -0
- package/skills/typescript-expert/SKILL.md +279 -0
- package/skills/ui-components-expert/SKILL.md +63 -0
- package/skills/ui-ux-pro-max/SKILL.md +201 -0
- package/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/skills/ui-ux-pro-max/data/colors.csv +97 -0
- package/skills/ui-ux-pro-max/data/icons.csv +101 -0
- package/skills/ui-ux-pro-max/data/landing.csv +31 -0
- package/skills/ui-ux-pro-max/data/products.csv +97 -0
- package/skills/ui-ux-pro-max/data/prompts.csv +24 -0
- package/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
- package/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/skills/ui-ux-pro-max/data/styles.csv +59 -0
- package/skills/ui-ux-pro-max/data/typography.csv +58 -0
- package/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
- package/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
- package/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-310.pyc +0 -0
- package/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-312.pyc +0 -0
- package/skills/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-310.pyc +0 -0
- package/skills/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-312.pyc +0 -0
- package/skills/ui-ux-pro-max/scripts/core.py +257 -0
- package/skills/ui-ux-pro-max/scripts/design_system.py +493 -0
- package/skills/ui-ux-pro-max/scripts/search.py +81 -0
- package/skills/ui_ux_expert/SKILL.md +114 -0
- package/skills/vibe-code-gardener/SKILL.md +173 -0
- package/skills/web-scraper/SKILL.md +205 -0
- package/skills/web-scraper/references/data-transforms.md +397 -0
- package/skills/web-scraper/references/extraction-patterns.md +475 -0
- package/skills/web-scraper/references/output-templates.md +481 -0
- package/skills/zero-to-prod-orchestrator/SKILL.md +180 -0
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
# React Patterns
|
|
2
|
+
|
|
3
|
+
Production-ready patterns for building scalable React applications with TypeScript.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Component Composition](#component-composition)
|
|
10
|
+
- [Custom Hooks](#custom-hooks)
|
|
11
|
+
- [State Management](#state-management)
|
|
12
|
+
- [Performance Patterns](#performance-patterns)
|
|
13
|
+
- [Error Boundaries](#error-boundaries)
|
|
14
|
+
- [Anti-Patterns](#anti-patterns)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Component Composition
|
|
19
|
+
|
|
20
|
+
### Compound Components
|
|
21
|
+
|
|
22
|
+
Use compound components when building reusable UI components with multiple related parts.
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
// Compound component pattern for a Select
|
|
26
|
+
interface SelectContextType {
|
|
27
|
+
value: string;
|
|
28
|
+
onChange: (value: string) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const SelectContext = createContext<SelectContextType | null>(null);
|
|
32
|
+
|
|
33
|
+
function Select({ children, value, onChange }: {
|
|
34
|
+
children: React.ReactNode;
|
|
35
|
+
value: string;
|
|
36
|
+
onChange: (value: string) => void;
|
|
37
|
+
}) {
|
|
38
|
+
return (
|
|
39
|
+
<SelectContext.Provider value={{ value, onChange }}>
|
|
40
|
+
<div className="relative">{children}</div>
|
|
41
|
+
</SelectContext.Provider>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function SelectTrigger({ children }: { children: React.ReactNode }) {
|
|
46
|
+
const context = useContext(SelectContext);
|
|
47
|
+
if (!context) throw new Error('SelectTrigger must be used within Select');
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<button className="flex items-center gap-2 px-4 py-2 border rounded">
|
|
51
|
+
{children}
|
|
52
|
+
</button>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function SelectOption({ value, children }: { value: string; children: React.ReactNode }) {
|
|
57
|
+
const context = useContext(SelectContext);
|
|
58
|
+
if (!context) throw new Error('SelectOption must be used within Select');
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div
|
|
62
|
+
onClick={() => context.onChange(value)}
|
|
63
|
+
className={`px-4 py-2 cursor-pointer hover:bg-gray-100 ${
|
|
64
|
+
context.value === value ? 'bg-blue-50' : ''
|
|
65
|
+
}`}
|
|
66
|
+
>
|
|
67
|
+
{children}
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Attach sub-components
|
|
73
|
+
Select.Trigger = SelectTrigger;
|
|
74
|
+
Select.Option = SelectOption;
|
|
75
|
+
|
|
76
|
+
// Usage
|
|
77
|
+
<Select value={selected} onChange={setSelected}>
|
|
78
|
+
<Select.Trigger>Choose option</Select.Trigger>
|
|
79
|
+
<Select.Option value="a">Option A</Select.Option>
|
|
80
|
+
<Select.Option value="b">Option B</Select.Option>
|
|
81
|
+
</Select>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Render Props
|
|
85
|
+
|
|
86
|
+
Use render props when you need to share behavior with flexible rendering.
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
interface MousePosition {
|
|
90
|
+
x: number;
|
|
91
|
+
y: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function MouseTracker({ render }: { render: (pos: MousePosition) => React.ReactNode }) {
|
|
95
|
+
const [position, setPosition] = useState<MousePosition>({ x: 0, y: 0 });
|
|
96
|
+
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
99
|
+
setPosition({ x: e.clientX, y: e.clientY });
|
|
100
|
+
};
|
|
101
|
+
window.addEventListener('mousemove', handleMouseMove);
|
|
102
|
+
return () => window.removeEventListener('mousemove', handleMouseMove);
|
|
103
|
+
}, []);
|
|
104
|
+
|
|
105
|
+
return <>{render(position)}</>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Usage
|
|
109
|
+
<MouseTracker
|
|
110
|
+
render={({ x, y }) => (
|
|
111
|
+
<div>Mouse position: {x}, {y}</div>
|
|
112
|
+
)}
|
|
113
|
+
/>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Higher-Order Components (HOC)
|
|
117
|
+
|
|
118
|
+
Use HOCs for cross-cutting concerns like authentication or logging.
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
function withAuth<P extends object>(WrappedComponent: React.ComponentType<P>) {
|
|
122
|
+
return function AuthenticatedComponent(props: P) {
|
|
123
|
+
const { user, isLoading } = useAuth();
|
|
124
|
+
|
|
125
|
+
if (isLoading) return <LoadingSpinner />;
|
|
126
|
+
if (!user) return <Navigate to="/login" />;
|
|
127
|
+
|
|
128
|
+
return <WrappedComponent {...props} />;
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Usage
|
|
133
|
+
const ProtectedDashboard = withAuth(Dashboard);
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Custom Hooks
|
|
139
|
+
|
|
140
|
+
### useAsync - Handle async operations
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
interface AsyncState<T> {
|
|
144
|
+
data: T | null;
|
|
145
|
+
error: Error | null;
|
|
146
|
+
status: 'idle' | 'loading' | 'success' | 'error';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function useAsync<T>(asyncFn: () => Promise<T>, deps: any[] = []) {
|
|
150
|
+
const [state, setState] = useState<AsyncState<T>>({
|
|
151
|
+
data: null,
|
|
152
|
+
error: null,
|
|
153
|
+
status: 'idle',
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const execute = useCallback(async () => {
|
|
157
|
+
setState({ data: null, error: null, status: 'loading' });
|
|
158
|
+
try {
|
|
159
|
+
const data = await asyncFn();
|
|
160
|
+
setState({ data, error: null, status: 'success' });
|
|
161
|
+
} catch (error) {
|
|
162
|
+
setState({ data: null, error: error as Error, status: 'error' });
|
|
163
|
+
}
|
|
164
|
+
}, deps);
|
|
165
|
+
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
execute();
|
|
168
|
+
}, [execute]);
|
|
169
|
+
|
|
170
|
+
return { ...state, refetch: execute };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Usage
|
|
174
|
+
function UserProfile({ userId }: { userId: string }) {
|
|
175
|
+
const { data: user, status, error, refetch } = useAsync(
|
|
176
|
+
() => fetchUser(userId),
|
|
177
|
+
[userId]
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
if (status === 'loading') return <Spinner />;
|
|
181
|
+
if (status === 'error') return <Error message={error?.message} />;
|
|
182
|
+
if (!user) return null;
|
|
183
|
+
|
|
184
|
+
return <Profile user={user} />;
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### useDebounce - Debounce values
|
|
189
|
+
|
|
190
|
+
```tsx
|
|
191
|
+
function useDebounce<T>(value: T, delay: number): T {
|
|
192
|
+
const [debouncedValue, setDebouncedValue] = useState(value);
|
|
193
|
+
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
const timer = setTimeout(() => setDebouncedValue(value), delay);
|
|
196
|
+
return () => clearTimeout(timer);
|
|
197
|
+
}, [value, delay]);
|
|
198
|
+
|
|
199
|
+
return debouncedValue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Usage
|
|
203
|
+
function SearchInput() {
|
|
204
|
+
const [query, setQuery] = useState('');
|
|
205
|
+
const debouncedQuery = useDebounce(query, 300);
|
|
206
|
+
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
if (debouncedQuery) {
|
|
209
|
+
searchAPI(debouncedQuery);
|
|
210
|
+
}
|
|
211
|
+
}, [debouncedQuery]);
|
|
212
|
+
|
|
213
|
+
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### useLocalStorage - Persist state
|
|
218
|
+
|
|
219
|
+
```tsx
|
|
220
|
+
function useLocalStorage<T>(key: string, initialValue: T) {
|
|
221
|
+
const [storedValue, setStoredValue] = useState<T>(() => {
|
|
222
|
+
if (typeof window === 'undefined') return initialValue;
|
|
223
|
+
try {
|
|
224
|
+
const item = window.localStorage.getItem(key);
|
|
225
|
+
return item ? JSON.parse(item) : initialValue;
|
|
226
|
+
} catch {
|
|
227
|
+
return initialValue;
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const setValue = useCallback((value: T | ((val: T) => T)) => {
|
|
232
|
+
try {
|
|
233
|
+
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
|
234
|
+
setStoredValue(valueToStore);
|
|
235
|
+
if (typeof window !== 'undefined') {
|
|
236
|
+
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
|
237
|
+
}
|
|
238
|
+
} catch (error) {
|
|
239
|
+
console.error('Error saving to localStorage:', error);
|
|
240
|
+
}
|
|
241
|
+
}, [key, storedValue]);
|
|
242
|
+
|
|
243
|
+
return [storedValue, setValue] as const;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Usage
|
|
247
|
+
const [theme, setTheme] = useLocalStorage('theme', 'light');
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### useMediaQuery - Responsive design
|
|
251
|
+
|
|
252
|
+
```tsx
|
|
253
|
+
function useMediaQuery(query: string): boolean {
|
|
254
|
+
const [matches, setMatches] = useState(false);
|
|
255
|
+
|
|
256
|
+
useEffect(() => {
|
|
257
|
+
const media = window.matchMedia(query);
|
|
258
|
+
setMatches(media.matches);
|
|
259
|
+
|
|
260
|
+
const listener = (e: MediaQueryListEvent) => setMatches(e.matches);
|
|
261
|
+
media.addEventListener('change', listener);
|
|
262
|
+
return () => media.removeEventListener('change', listener);
|
|
263
|
+
}, [query]);
|
|
264
|
+
|
|
265
|
+
return matches;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Usage
|
|
269
|
+
function ResponsiveNav() {
|
|
270
|
+
const isMobile = useMediaQuery('(max-width: 768px)');
|
|
271
|
+
return isMobile ? <MobileNav /> : <DesktopNav />;
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### usePrevious - Track previous values
|
|
276
|
+
|
|
277
|
+
```tsx
|
|
278
|
+
function usePrevious<T>(value: T): T | undefined {
|
|
279
|
+
const ref = useRef<T>();
|
|
280
|
+
|
|
281
|
+
useEffect(() => {
|
|
282
|
+
ref.current = value;
|
|
283
|
+
}, [value]);
|
|
284
|
+
|
|
285
|
+
return ref.current;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Usage
|
|
289
|
+
function Counter() {
|
|
290
|
+
const [count, setCount] = useState(0);
|
|
291
|
+
const prevCount = usePrevious(count);
|
|
292
|
+
|
|
293
|
+
return (
|
|
294
|
+
<div>
|
|
295
|
+
Current: {count}, Previous: {prevCount}
|
|
296
|
+
</div>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## State Management
|
|
304
|
+
|
|
305
|
+
### Context with Reducer
|
|
306
|
+
|
|
307
|
+
For complex state that multiple components need to access.
|
|
308
|
+
|
|
309
|
+
```tsx
|
|
310
|
+
// types.ts
|
|
311
|
+
interface CartItem {
|
|
312
|
+
id: string;
|
|
313
|
+
name: string;
|
|
314
|
+
price: number;
|
|
315
|
+
quantity: number;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
interface CartState {
|
|
319
|
+
items: CartItem[];
|
|
320
|
+
total: number;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
type CartAction =
|
|
324
|
+
| { type: 'ADD_ITEM'; payload: CartItem }
|
|
325
|
+
| { type: 'REMOVE_ITEM'; payload: string }
|
|
326
|
+
| { type: 'UPDATE_QUANTITY'; payload: { id: string; quantity: number } }
|
|
327
|
+
| { type: 'CLEAR_CART' };
|
|
328
|
+
|
|
329
|
+
// reducer.ts
|
|
330
|
+
function cartReducer(state: CartState, action: CartAction): CartState {
|
|
331
|
+
switch (action.type) {
|
|
332
|
+
case 'ADD_ITEM': {
|
|
333
|
+
const existingItem = state.items.find(i => i.id === action.payload.id);
|
|
334
|
+
if (existingItem) {
|
|
335
|
+
return {
|
|
336
|
+
...state,
|
|
337
|
+
items: state.items.map(item =>
|
|
338
|
+
item.id === action.payload.id
|
|
339
|
+
? { ...item, quantity: item.quantity + 1 }
|
|
340
|
+
: item
|
|
341
|
+
),
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
...state,
|
|
346
|
+
items: [...state.items, { ...action.payload, quantity: 1 }],
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
case 'REMOVE_ITEM':
|
|
350
|
+
return {
|
|
351
|
+
...state,
|
|
352
|
+
items: state.items.filter(i => i.id !== action.payload),
|
|
353
|
+
};
|
|
354
|
+
case 'UPDATE_QUANTITY':
|
|
355
|
+
return {
|
|
356
|
+
...state,
|
|
357
|
+
items: state.items.map(item =>
|
|
358
|
+
item.id === action.payload.id
|
|
359
|
+
? { ...item, quantity: action.payload.quantity }
|
|
360
|
+
: item
|
|
361
|
+
),
|
|
362
|
+
};
|
|
363
|
+
case 'CLEAR_CART':
|
|
364
|
+
return { items: [], total: 0 };
|
|
365
|
+
default:
|
|
366
|
+
return state;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// context.tsx
|
|
371
|
+
const CartContext = createContext<{
|
|
372
|
+
state: CartState;
|
|
373
|
+
dispatch: React.Dispatch<CartAction>;
|
|
374
|
+
} | null>(null);
|
|
375
|
+
|
|
376
|
+
function CartProvider({ children }: { children: React.ReactNode }) {
|
|
377
|
+
const [state, dispatch] = useReducer(cartReducer, { items: [], total: 0 });
|
|
378
|
+
|
|
379
|
+
// Compute total whenever items change
|
|
380
|
+
const stateWithTotal = useMemo(() => ({
|
|
381
|
+
...state,
|
|
382
|
+
total: state.items.reduce((sum, item) => sum + item.price * item.quantity, 0),
|
|
383
|
+
}), [state.items]);
|
|
384
|
+
|
|
385
|
+
return (
|
|
386
|
+
<CartContext.Provider value={{ state: stateWithTotal, dispatch }}>
|
|
387
|
+
{children}
|
|
388
|
+
</CartContext.Provider>
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function useCart() {
|
|
393
|
+
const context = useContext(CartContext);
|
|
394
|
+
if (!context) throw new Error('useCart must be used within CartProvider');
|
|
395
|
+
return context;
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### Zustand (Lightweight Alternative)
|
|
400
|
+
|
|
401
|
+
```tsx
|
|
402
|
+
import { create } from 'zustand';
|
|
403
|
+
import { persist } from 'zustand/middleware';
|
|
404
|
+
|
|
405
|
+
interface AuthStore {
|
|
406
|
+
user: User | null;
|
|
407
|
+
token: string | null;
|
|
408
|
+
login: (email: string, password: string) => Promise<void>;
|
|
409
|
+
logout: () => void;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const useAuthStore = create<AuthStore>()(
|
|
413
|
+
persist(
|
|
414
|
+
(set) => ({
|
|
415
|
+
user: null,
|
|
416
|
+
token: null,
|
|
417
|
+
login: async (email, password) => {
|
|
418
|
+
const { user, token } = await authAPI.login(email, password);
|
|
419
|
+
set({ user, token });
|
|
420
|
+
},
|
|
421
|
+
logout: () => set({ user: null, token: null }),
|
|
422
|
+
}),
|
|
423
|
+
{ name: 'auth-storage' }
|
|
424
|
+
)
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
// Usage
|
|
428
|
+
function Profile() {
|
|
429
|
+
const { user, logout } = useAuthStore();
|
|
430
|
+
return user ? <div>{user.name} <button onClick={logout}>Logout</button></div> : null;
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
---
|
|
435
|
+
|
|
436
|
+
## Performance Patterns
|
|
437
|
+
|
|
438
|
+
### React.memo with Custom Comparison
|
|
439
|
+
|
|
440
|
+
```tsx
|
|
441
|
+
interface ListItemProps {
|
|
442
|
+
item: { id: string; name: string; count: number };
|
|
443
|
+
onSelect: (id: string) => void;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const ListItem = React.memo(
|
|
447
|
+
function ListItem({ item, onSelect }: ListItemProps) {
|
|
448
|
+
return (
|
|
449
|
+
<div onClick={() => onSelect(item.id)}>
|
|
450
|
+
{item.name} ({item.count})
|
|
451
|
+
</div>
|
|
452
|
+
);
|
|
453
|
+
},
|
|
454
|
+
(prevProps, nextProps) => {
|
|
455
|
+
// Only re-render if item data changed
|
|
456
|
+
return (
|
|
457
|
+
prevProps.item.id === nextProps.item.id &&
|
|
458
|
+
prevProps.item.name === nextProps.item.name &&
|
|
459
|
+
prevProps.item.count === nextProps.item.count
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
);
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### useMemo for Expensive Calculations
|
|
466
|
+
|
|
467
|
+
```tsx
|
|
468
|
+
function DataTable({ data, sortColumn, filterText }: {
|
|
469
|
+
data: Item[];
|
|
470
|
+
sortColumn: string;
|
|
471
|
+
filterText: string;
|
|
472
|
+
}) {
|
|
473
|
+
const processedData = useMemo(() => {
|
|
474
|
+
// Filter
|
|
475
|
+
let result = data.filter(item =>
|
|
476
|
+
item.name.toLowerCase().includes(filterText.toLowerCase())
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
// Sort
|
|
480
|
+
result = [...result].sort((a, b) => {
|
|
481
|
+
const aVal = a[sortColumn as keyof Item];
|
|
482
|
+
const bVal = b[sortColumn as keyof Item];
|
|
483
|
+
return aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
return result;
|
|
487
|
+
}, [data, sortColumn, filterText]);
|
|
488
|
+
|
|
489
|
+
return (
|
|
490
|
+
<table>
|
|
491
|
+
{processedData.map(item => (
|
|
492
|
+
<tr key={item.id}>{/* ... */}</tr>
|
|
493
|
+
))}
|
|
494
|
+
</table>
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
### useCallback for Stable References
|
|
500
|
+
|
|
501
|
+
```tsx
|
|
502
|
+
function ParentComponent() {
|
|
503
|
+
const [items, setItems] = useState<Item[]>([]);
|
|
504
|
+
|
|
505
|
+
// Stable reference - won't cause child re-renders
|
|
506
|
+
const handleItemClick = useCallback((id: string) => {
|
|
507
|
+
setItems(prev => prev.map(item =>
|
|
508
|
+
item.id === id ? { ...item, selected: !item.selected } : item
|
|
509
|
+
));
|
|
510
|
+
}, []);
|
|
511
|
+
|
|
512
|
+
const handleAddItem = useCallback((newItem: Item) => {
|
|
513
|
+
setItems(prev => [...prev, newItem]);
|
|
514
|
+
}, []);
|
|
515
|
+
|
|
516
|
+
return (
|
|
517
|
+
<>
|
|
518
|
+
<ItemList items={items} onItemClick={handleItemClick} />
|
|
519
|
+
<AddItemForm onAdd={handleAddItem} />
|
|
520
|
+
</>
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### Virtualization for Long Lists
|
|
526
|
+
|
|
527
|
+
```tsx
|
|
528
|
+
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
529
|
+
|
|
530
|
+
function VirtualList({ items }: { items: Item[] }) {
|
|
531
|
+
const parentRef = useRef<HTMLDivElement>(null);
|
|
532
|
+
|
|
533
|
+
const virtualizer = useVirtualizer({
|
|
534
|
+
count: items.length,
|
|
535
|
+
getScrollElement: () => parentRef.current,
|
|
536
|
+
estimateSize: () => 50, // estimated row height
|
|
537
|
+
overscan: 5,
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
return (
|
|
541
|
+
<div ref={parentRef} className="h-[400px] overflow-auto">
|
|
542
|
+
<div
|
|
543
|
+
style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}
|
|
544
|
+
>
|
|
545
|
+
{virtualizer.getVirtualItems().map(virtualRow => (
|
|
546
|
+
<div
|
|
547
|
+
key={virtualRow.key}
|
|
548
|
+
style={{
|
|
549
|
+
position: 'absolute',
|
|
550
|
+
top: 0,
|
|
551
|
+
left: 0,
|
|
552
|
+
width: '100%',
|
|
553
|
+
height: `${virtualRow.size}px`,
|
|
554
|
+
transform: `translateY(${virtualRow.start}px)`,
|
|
555
|
+
}}
|
|
556
|
+
>
|
|
557
|
+
{items[virtualRow.index].name}
|
|
558
|
+
</div>
|
|
559
|
+
))}
|
|
560
|
+
</div>
|
|
561
|
+
</div>
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
---
|
|
567
|
+
|
|
568
|
+
## Error Boundaries
|
|
569
|
+
|
|
570
|
+
### Class-Based Error Boundary
|
|
571
|
+
|
|
572
|
+
```tsx
|
|
573
|
+
interface ErrorBoundaryProps {
|
|
574
|
+
children: React.ReactNode;
|
|
575
|
+
fallback?: React.ReactNode;
|
|
576
|
+
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
interface ErrorBoundaryState {
|
|
580
|
+
hasError: boolean;
|
|
581
|
+
error: Error | null;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
585
|
+
state: ErrorBoundaryState = { hasError: false, error: null };
|
|
586
|
+
|
|
587
|
+
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
|
588
|
+
return { hasError: true, error };
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
|
592
|
+
this.props.onError?.(error, errorInfo);
|
|
593
|
+
// Log to error reporting service
|
|
594
|
+
console.error('Error caught:', error, errorInfo);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
render() {
|
|
598
|
+
if (this.state.hasError) {
|
|
599
|
+
return this.props.fallback || (
|
|
600
|
+
<div className="p-4 bg-red-50 border border-red-200 rounded">
|
|
601
|
+
<h2 className="text-red-800 font-bold">Something went wrong</h2>
|
|
602
|
+
<p className="text-red-600">{this.state.error?.message}</p>
|
|
603
|
+
<button
|
|
604
|
+
onClick={() => this.setState({ hasError: false, error: null })}
|
|
605
|
+
className="mt-2 px-4 py-2 bg-red-600 text-white rounded"
|
|
606
|
+
>
|
|
607
|
+
Try Again
|
|
608
|
+
</button>
|
|
609
|
+
</div>
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
return this.props.children;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Usage
|
|
618
|
+
<ErrorBoundary
|
|
619
|
+
fallback={<ErrorFallback />}
|
|
620
|
+
onError={(error) => trackError(error)}
|
|
621
|
+
>
|
|
622
|
+
<MyComponent />
|
|
623
|
+
</ErrorBoundary>
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
### Suspense with Error Boundary
|
|
627
|
+
|
|
628
|
+
```tsx
|
|
629
|
+
function DataComponent() {
|
|
630
|
+
return (
|
|
631
|
+
<ErrorBoundary fallback={<ErrorMessage />}>
|
|
632
|
+
<Suspense fallback={<LoadingSpinner />}>
|
|
633
|
+
<AsyncDataLoader />
|
|
634
|
+
</Suspense>
|
|
635
|
+
</ErrorBoundary>
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
---
|
|
641
|
+
|
|
642
|
+
## Anti-Patterns
|
|
643
|
+
|
|
644
|
+
### Avoid: Inline Object/Array Creation in JSX
|
|
645
|
+
|
|
646
|
+
```tsx
|
|
647
|
+
// BAD - Creates new object every render, causes re-renders
|
|
648
|
+
<Component style={{ color: 'red' }} items={[1, 2, 3]} />
|
|
649
|
+
|
|
650
|
+
// GOOD - Define outside or use useMemo
|
|
651
|
+
const style = { color: 'red' };
|
|
652
|
+
const items = [1, 2, 3];
|
|
653
|
+
<Component style={style} items={items} />
|
|
654
|
+
|
|
655
|
+
// Or with useMemo for dynamic values
|
|
656
|
+
const style = useMemo(() => ({ color: theme.primary }), [theme.primary]);
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
### Avoid: Index as Key for Dynamic Lists
|
|
660
|
+
|
|
661
|
+
```tsx
|
|
662
|
+
// BAD - Index keys break with reordering/filtering
|
|
663
|
+
{items.map((item, index) => (
|
|
664
|
+
<Item key={index} data={item} />
|
|
665
|
+
))}
|
|
666
|
+
|
|
667
|
+
// GOOD - Use stable unique ID
|
|
668
|
+
{items.map(item => (
|
|
669
|
+
<Item key={item.id} data={item} />
|
|
670
|
+
))}
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
### Avoid: Prop Drilling
|
|
674
|
+
|
|
675
|
+
```tsx
|
|
676
|
+
// BAD - Passing props through many levels
|
|
677
|
+
<App user={user}>
|
|
678
|
+
<Layout user={user}>
|
|
679
|
+
<Sidebar user={user}>
|
|
680
|
+
<UserInfo user={user} />
|
|
681
|
+
</Sidebar>
|
|
682
|
+
</Layout>
|
|
683
|
+
</App>
|
|
684
|
+
|
|
685
|
+
// GOOD - Use Context
|
|
686
|
+
const UserContext = createContext<User | null>(null);
|
|
687
|
+
|
|
688
|
+
function App() {
|
|
689
|
+
return (
|
|
690
|
+
<UserContext.Provider value={user}>
|
|
691
|
+
<Layout>
|
|
692
|
+
<Sidebar>
|
|
693
|
+
<UserInfo />
|
|
694
|
+
</Sidebar>
|
|
695
|
+
</Layout>
|
|
696
|
+
</UserContext.Provider>
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function UserInfo() {
|
|
701
|
+
const user = useContext(UserContext);
|
|
702
|
+
return <div>{user?.name}</div>;
|
|
703
|
+
}
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
### Avoid: Mutating State Directly
|
|
707
|
+
|
|
708
|
+
```tsx
|
|
709
|
+
// BAD - Mutates state directly
|
|
710
|
+
const addItem = (item: Item) => {
|
|
711
|
+
items.push(item); // WRONG
|
|
712
|
+
setItems(items); // Won't trigger re-render
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
// GOOD - Create new array
|
|
716
|
+
const addItem = (item: Item) => {
|
|
717
|
+
setItems(prev => [...prev, item]);
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
// GOOD - For objects
|
|
721
|
+
const updateUser = (field: string, value: string) => {
|
|
722
|
+
setUser(prev => ({ ...prev, [field]: value }));
|
|
723
|
+
};
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
### Avoid: useEffect for Derived State
|
|
727
|
+
|
|
728
|
+
```tsx
|
|
729
|
+
// BAD - Unnecessary effect and extra render
|
|
730
|
+
const [items, setItems] = useState<Item[]>([]);
|
|
731
|
+
const [total, setTotal] = useState(0);
|
|
732
|
+
|
|
733
|
+
useEffect(() => {
|
|
734
|
+
setTotal(items.reduce((sum, item) => sum + item.price, 0));
|
|
735
|
+
}, [items]);
|
|
736
|
+
|
|
737
|
+
// GOOD - Compute during render
|
|
738
|
+
const [items, setItems] = useState<Item[]>([]);
|
|
739
|
+
const total = items.reduce((sum, item) => sum + item.price, 0);
|
|
740
|
+
|
|
741
|
+
// Or useMemo for expensive calculations
|
|
742
|
+
const total = useMemo(
|
|
743
|
+
() => items.reduce((sum, item) => sum + item.price, 0),
|
|
744
|
+
[items]
|
|
745
|
+
);
|
|
746
|
+
```
|