clearctx 3.0.0 → 3.1.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 +1 -0
- package/bin/setup.js +33 -1
- package/package.json +3 -2
- package/skills/api-design/SKILL.md +796 -0
- package/skills/devops/SKILL.md +1043 -0
- package/skills/index.json +53 -0
- package/skills/nodejs-backend/SKILL.md +853 -0
- package/skills/postgresql/SKILL.md +315 -0
- package/skills/react-frontend/SKILL.md +683 -0
- package/skills/security/SKILL.md +1000 -0
- package/skills/testing-qa/SKILL.md +842 -0
- package/skills/typescript/SKILL.md +932 -0
- package/src/mcp-server.js +126 -1
- package/src/prompts.js +47 -2
- package/src/skill-registry.js +182 -0
- package/src/stream-session.js +22 -2
- package/STRATEGY.md +0 -485
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: react-frontend
|
|
3
|
+
description: Production-grade React component architecture, state management, hooks, performance, and accessibility patterns
|
|
4
|
+
domain: frontend
|
|
5
|
+
keywords: [react, frontend, components, hooks, state-management, accessibility, performance, routing]
|
|
6
|
+
version: 1.0.0
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# React Frontend — Expertise Guide
|
|
10
|
+
|
|
11
|
+
## Worker Context
|
|
12
|
+
|
|
13
|
+
You are a **React frontend specialist** building production-grade user interfaces. Your domain is client-side React applications with modern patterns (hooks, functional components, composition). You coordinate with backend workers via API contracts and publish component structures as artifacts.
|
|
14
|
+
|
|
15
|
+
### Component Architecture
|
|
16
|
+
|
|
17
|
+
**Atomic Design Hierarchy:**
|
|
18
|
+
- **Atoms:** Button, Input, Icon (single-purpose, no business logic)
|
|
19
|
+
- **Molecules:** SearchBar (Input + Button), FormField (Label + Input + Error)
|
|
20
|
+
- **Organisms:** Header, ProductCard, LoginForm (complete UI sections)
|
|
21
|
+
- **Templates:** Page layouts with placeholder content
|
|
22
|
+
- **Pages:** Route-level components with real data
|
|
23
|
+
|
|
24
|
+
**Container/Presenter Pattern:**
|
|
25
|
+
```jsx
|
|
26
|
+
// GOOD: Container handles data, Presenter handles UI
|
|
27
|
+
function UserListContainer() {
|
|
28
|
+
const { data, isLoading, error } = useQuery('users', fetchUsers);
|
|
29
|
+
return <UserListPresenter users={data} loading={isLoading} error={error} />;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function UserListPresenter({ users, loading, error }) {
|
|
33
|
+
if (loading) return <Skeleton />;
|
|
34
|
+
if (error) return <ErrorMessage error={error} />;
|
|
35
|
+
return <ul>{users.map(u => <UserCard key={u.id} user={u} />)}</ul>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// BAD: Mixed data fetching and UI in same component
|
|
39
|
+
function UserList() {
|
|
40
|
+
const [users, setUsers] = useState([]);
|
|
41
|
+
useEffect(() => { fetch('/users').then(r => r.json()).then(setUsers); }, []);
|
|
42
|
+
return <ul>{users.map(u => <li>{u.name}</li>)}</ul>;
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Compound Components (for related UI):**
|
|
47
|
+
```jsx
|
|
48
|
+
// GOOD: Related controls grouped with shared context
|
|
49
|
+
<Tabs>
|
|
50
|
+
<TabList>
|
|
51
|
+
<Tab>Profile</Tab>
|
|
52
|
+
<Tab>Settings</Tab>
|
|
53
|
+
</TabList>
|
|
54
|
+
<TabPanels>
|
|
55
|
+
<TabPanel><ProfileForm /></TabPanel>
|
|
56
|
+
<TabPanel><SettingsForm /></TabPanel>
|
|
57
|
+
</TabPanels>
|
|
58
|
+
</Tabs>
|
|
59
|
+
|
|
60
|
+
// Implementation uses Context to share state
|
|
61
|
+
const TabsContext = createContext();
|
|
62
|
+
function Tabs({ children }) {
|
|
63
|
+
const [activeTab, setActiveTab] = useState(0);
|
|
64
|
+
return <TabsContext.Provider value={{ activeTab, setActiveTab }}>{children}</TabsContext.Provider>;
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### State Management Decision Tree
|
|
69
|
+
|
|
70
|
+
| Scenario | Solution | When to Use |
|
|
71
|
+
|----------|----------|-------------|
|
|
72
|
+
| Component-local toggle/counter | `useState` | State never leaves component |
|
|
73
|
+
| Component-local complex state machine | `useReducer` | State has complex transitions (cart, wizard) |
|
|
74
|
+
| Shared between siblings | Lift to parent | 2-3 components need same data |
|
|
75
|
+
| Shared across component tree | Context + useReducer | Deep prop drilling (>3 levels), theme/auth |
|
|
76
|
+
| Async data with caching | TanStack Query / SWR | API calls, need deduplication/refetch |
|
|
77
|
+
| Global complex state | Zustand | Multiple features share state, simple API vs Redux |
|
|
78
|
+
|
|
79
|
+
**CRITICAL:** NEVER add external state management (Zustand, Redux) until Context proves insufficient. 90% of apps only need useState + Context + TanStack Query.
|
|
80
|
+
|
|
81
|
+
```jsx
|
|
82
|
+
// GOOD: Context for auth, TanStack Query for data
|
|
83
|
+
const AuthContext = createContext();
|
|
84
|
+
function AuthProvider({ children }) {
|
|
85
|
+
const [user, setUser] = useState(null);
|
|
86
|
+
return <AuthContext.Provider value={{ user, setUser }}>{children}</AuthContext.Provider>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function UserProfile() {
|
|
90
|
+
const { user } = useContext(AuthContext);
|
|
91
|
+
const { data: profile } = useQuery(['profile', user.id], () => fetchProfile(user.id));
|
|
92
|
+
return <div>{profile.name}</div>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// BAD: Redux for simple auth state
|
|
96
|
+
const authSlice = createSlice({ name: 'auth', initialState: { user: null }, reducers: { setUser: ... } });
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Hooks Patterns
|
|
100
|
+
|
|
101
|
+
**Custom Hooks for Reusable Logic:**
|
|
102
|
+
```jsx
|
|
103
|
+
// GOOD: Extract reusable logic into hooks
|
|
104
|
+
function useDebounce(value, delay) {
|
|
105
|
+
const [debounced, setDebounced] = useState(value);
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
const timer = setTimeout(() => setDebounced(value), delay);
|
|
108
|
+
return () => clearTimeout(timer); // CRITICAL: Cleanup
|
|
109
|
+
}, [value, delay]); // CRITICAL: Exhaustive deps
|
|
110
|
+
return debounced;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function SearchInput() {
|
|
114
|
+
const [query, setQuery] = useState('');
|
|
115
|
+
const debouncedQuery = useDebounce(query, 300);
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (debouncedQuery) search(debouncedQuery);
|
|
118
|
+
}, [debouncedQuery]);
|
|
119
|
+
return <input value={query} onChange={e => setQuery(e.target.value)} />;
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Hook Rules (NEVER violate):**
|
|
124
|
+
- NEVER call hooks conditionally: `if (condition) { useState(...); }` breaks React
|
|
125
|
+
- NEVER call hooks in loops or callbacks
|
|
126
|
+
- Dependency arrays MUST be exhaustive (all variables from outer scope)
|
|
127
|
+
- Cleanup functions MUST cancel subscriptions/timers/requests
|
|
128
|
+
|
|
129
|
+
```jsx
|
|
130
|
+
// BAD: Conditional hook call
|
|
131
|
+
function User({ id }) {
|
|
132
|
+
if (!id) return null;
|
|
133
|
+
const user = useQuery(['user', id], ...); // BREAKS: Hook not called every render
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// GOOD: Hook always called, condition inside
|
|
137
|
+
function User({ id }) {
|
|
138
|
+
const user = useQuery(['user', id], ..., { enabled: !!id });
|
|
139
|
+
if (!id) return null;
|
|
140
|
+
return <div>{user.data.name}</div>;
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Performance
|
|
145
|
+
|
|
146
|
+
**React.memo — ONLY for:**
|
|
147
|
+
- Components receiving complex props (objects/arrays) AND
|
|
148
|
+
- Components rendering frequently (parent re-renders often)
|
|
149
|
+
|
|
150
|
+
```jsx
|
|
151
|
+
// GOOD: Memoized child receiving complex prop
|
|
152
|
+
const ExpensiveList = React.memo(({ items }) => (
|
|
153
|
+
<ul>{items.map(item => <li key={item.id}>{item.name}</li>)}</ul>
|
|
154
|
+
));
|
|
155
|
+
|
|
156
|
+
function Parent() {
|
|
157
|
+
const [count, setCount] = useState(0);
|
|
158
|
+
const items = useMemo(() => expensiveComputation(), []); // Stable reference
|
|
159
|
+
return <div><button onClick={() => setCount(count + 1)}>{count}</button><ExpensiveList items={items} /></div>;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// BAD: Premature memoization without profiling
|
|
163
|
+
const Button = React.memo(({ onClick, children }) => <button onClick={onClick}>{children}</button>);
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**useMemo/useCallback — ONLY when:**
|
|
167
|
+
- Passing to memoized children (prevents their re-render)
|
|
168
|
+
- Expensive computation (measured with profiler)
|
|
169
|
+
- Dependency in useEffect (prevent infinite loops)
|
|
170
|
+
|
|
171
|
+
**IMPORTANT:** Premature memoization adds complexity with zero benefit. Profile with React DevTools first. Most components render in <1ms — memoization overhead exceeds savings.
|
|
172
|
+
|
|
173
|
+
### Forms
|
|
174
|
+
|
|
175
|
+
**Decision Tree:**
|
|
176
|
+
|
|
177
|
+
| Form Complexity | Solution | Example |
|
|
178
|
+
|-----------------|----------|---------|
|
|
179
|
+
| 1-3 fields, simple validation | Controlled `useState` | Login, search |
|
|
180
|
+
| 4+ fields, complex validation | react-hook-form + Zod | Signup, profile edit |
|
|
181
|
+
| Multi-step forms | react-hook-form + useReducer | Onboarding wizard |
|
|
182
|
+
|
|
183
|
+
```jsx
|
|
184
|
+
// GOOD: react-hook-form + Zod for complex forms
|
|
185
|
+
import { useForm } from 'react-hook-form';
|
|
186
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
187
|
+
import { z } from 'zod';
|
|
188
|
+
|
|
189
|
+
const schema = z.object({
|
|
190
|
+
email: z.string().email('Invalid email'),
|
|
191
|
+
password: z.string().min(8, 'Min 8 characters'),
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
function SignupForm() {
|
|
195
|
+
const { register, handleSubmit, formState: { errors } } = useForm({
|
|
196
|
+
resolver: zodResolver(schema),
|
|
197
|
+
mode: 'onBlur', // IMPORTANT: Show errors on blur, not onChange
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const onSubmit = (data) => createUser(data);
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<form onSubmit={handleSubmit(onSubmit)}>
|
|
204
|
+
<input {...register('email')} aria-invalid={!!errors.email} />
|
|
205
|
+
{errors.email && <span role="alert">{errors.email.message}</span>}
|
|
206
|
+
<input type="password" {...register('password')} />
|
|
207
|
+
{errors.password && <span role="alert">{errors.password.message}</span>}
|
|
208
|
+
<button type="submit">Sign Up</button>
|
|
209
|
+
</form>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Routing
|
|
215
|
+
|
|
216
|
+
**React Router v6+ Patterns:**
|
|
217
|
+
```jsx
|
|
218
|
+
// GOOD: Lazy loading + protected routes + nested layouts
|
|
219
|
+
import { lazy, Suspense } from 'react';
|
|
220
|
+
import { createBrowserRouter, RouterProvider, Outlet, Navigate } from 'react-router-dom';
|
|
221
|
+
|
|
222
|
+
const Dashboard = lazy(() => import('./pages/Dashboard'));
|
|
223
|
+
const Profile = lazy(() => import('./pages/Profile'));
|
|
224
|
+
|
|
225
|
+
function ProtectedRoute({ children }) {
|
|
226
|
+
const { user } = useAuth();
|
|
227
|
+
return user ? children : <Navigate to="/login" replace />;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function DashboardLayout() {
|
|
231
|
+
return <div><Sidebar /><Outlet /></div>; // Outlet renders child routes
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const router = createBrowserRouter([
|
|
235
|
+
{ path: '/login', element: <Login /> },
|
|
236
|
+
{
|
|
237
|
+
path: '/dashboard',
|
|
238
|
+
element: <ProtectedRoute><DashboardLayout /></ProtectedRoute>,
|
|
239
|
+
children: [
|
|
240
|
+
{ index: true, element: <Suspense fallback={<Spinner />}><Dashboard /></Suspense> },
|
|
241
|
+
{ path: 'profile', element: <Suspense fallback={<Spinner />}><Profile /></Suspense> },
|
|
242
|
+
],
|
|
243
|
+
},
|
|
244
|
+
]);
|
|
245
|
+
|
|
246
|
+
function App() {
|
|
247
|
+
return <RouterProvider router={router} />;
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Styling
|
|
252
|
+
|
|
253
|
+
**Prefer:** Tailwind CSS (utility-first, no style conflicts, tree-shakeable)
|
|
254
|
+
|
|
255
|
+
**If project uses CSS Modules:** Stick with it. NEVER mix styling approaches.
|
|
256
|
+
|
|
257
|
+
```jsx
|
|
258
|
+
// GOOD: Tailwind for new projects
|
|
259
|
+
<button className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 focus:ring-2 focus:ring-blue-500">
|
|
260
|
+
Submit
|
|
261
|
+
</button>
|
|
262
|
+
|
|
263
|
+
// GOOD: CSS Modules if already in project
|
|
264
|
+
import styles from './Button.module.css';
|
|
265
|
+
<button className={styles.primary}>Submit</button>
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Error Boundaries
|
|
269
|
+
|
|
270
|
+
```jsx
|
|
271
|
+
// GOOD: Route-level error boundary with retry
|
|
272
|
+
class ErrorBoundary extends Component {
|
|
273
|
+
state = { hasError: false, error: null };
|
|
274
|
+
|
|
275
|
+
static getDerivedStateFromError(error) {
|
|
276
|
+
return { hasError: true, error };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
componentDidCatch(error, info) {
|
|
280
|
+
logErrorToService(error, info); // IMPORTANT: Send to monitoring
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
render() {
|
|
284
|
+
if (this.state.hasError) {
|
|
285
|
+
return (
|
|
286
|
+
<div role="alert">
|
|
287
|
+
<h2>Something went wrong</h2>
|
|
288
|
+
<button onClick={() => this.setState({ hasError: false })}>Try again</button>
|
|
289
|
+
</div>
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
return this.props.children;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Wrap route-level and feature-level boundaries
|
|
297
|
+
<ErrorBoundary><Dashboard /></ErrorBoundary>
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Data Fetching
|
|
301
|
+
|
|
302
|
+
**Prefer:** TanStack Query (automatic caching, deduplication, refetch, background updates)
|
|
303
|
+
|
|
304
|
+
```jsx
|
|
305
|
+
// GOOD: TanStack Query handles all 3 states + caching
|
|
306
|
+
function UserProfile({ userId }) {
|
|
307
|
+
const { data, isLoading, error, refetch } = useQuery(
|
|
308
|
+
['user', userId],
|
|
309
|
+
() => fetchUser(userId),
|
|
310
|
+
{ staleTime: 5 * 60 * 1000 } // Cache for 5 min
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
if (isLoading) return <Skeleton />;
|
|
314
|
+
if (error) return <ErrorMessage error={error} onRetry={refetch} />;
|
|
315
|
+
return <div>{data.name}</div>;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// BAD: useEffect without cleanup/cancellation
|
|
319
|
+
function UserProfile({ userId }) {
|
|
320
|
+
const [user, setUser] = useState(null);
|
|
321
|
+
useEffect(() => {
|
|
322
|
+
fetch(`/users/${userId}`).then(r => r.json()).then(setUser); // No cleanup!
|
|
323
|
+
}, [userId]);
|
|
324
|
+
return <div>{user?.name}</div>; // No loading/error states
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
**CRITICAL:** Handle ALL 3 states: loading (skeleton), error (retry button), success (data). NEVER fetch in useEffect without AbortController cleanup.
|
|
329
|
+
|
|
330
|
+
### Accessibility
|
|
331
|
+
|
|
332
|
+
| Element | Requirement | Example |
|
|
333
|
+
|---------|-------------|---------|
|
|
334
|
+
| Icon buttons | `aria-label` | `<button aria-label="Close"><X /></button>` |
|
|
335
|
+
| Form inputs | Associated `<label>` | `<label htmlFor="email">Email</label><input id="email" />` |
|
|
336
|
+
| Error messages | `role="alert"` | `<span role="alert">{error}</span>` |
|
|
337
|
+
| Modals | Focus trap + Esc key | Use @react-aria or @headlessui |
|
|
338
|
+
| Interactive divs | Use `<button>` instead | NEVER `<div onClick={...}>` |
|
|
339
|
+
| Color contrast | 4.5:1 minimum | Use WebAIM Contrast Checker |
|
|
340
|
+
|
|
341
|
+
```jsx
|
|
342
|
+
// GOOD: Accessible modal
|
|
343
|
+
import { Dialog } from '@headlessui/react';
|
|
344
|
+
|
|
345
|
+
function Modal({ isOpen, onClose }) {
|
|
346
|
+
return (
|
|
347
|
+
<Dialog open={isOpen} onClose={onClose}>
|
|
348
|
+
<Dialog.Panel>
|
|
349
|
+
<Dialog.Title>Confirm Action</Dialog.Title>
|
|
350
|
+
<p>Are you sure?</p>
|
|
351
|
+
<button onClick={onClose}>Cancel</button>
|
|
352
|
+
<button onClick={handleConfirm}>Confirm</button>
|
|
353
|
+
</Dialog.Panel>
|
|
354
|
+
</Dialog>
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### File Structure
|
|
360
|
+
|
|
361
|
+
```
|
|
362
|
+
src/
|
|
363
|
+
├── features/
|
|
364
|
+
│ ├── auth/
|
|
365
|
+
│ │ ├── components/
|
|
366
|
+
│ │ │ ├── LoginForm.jsx
|
|
367
|
+
│ │ │ └── SignupForm.jsx
|
|
368
|
+
│ │ ├── hooks/
|
|
369
|
+
│ │ │ └── useAuth.js
|
|
370
|
+
│ │ ├── api/
|
|
371
|
+
│ │ │ └── authApi.js
|
|
372
|
+
│ │ └── index.js (barrel export)
|
|
373
|
+
│ └── users/
|
|
374
|
+
│ ├── components/
|
|
375
|
+
│ ├── hooks/
|
|
376
|
+
│ └── index.js
|
|
377
|
+
├── components/ (shared UI)
|
|
378
|
+
│ ├── Button.jsx
|
|
379
|
+
│ ├── Modal.jsx
|
|
380
|
+
│ └── index.js
|
|
381
|
+
├── hooks/ (shared logic)
|
|
382
|
+
│ ├── useDebounce.js
|
|
383
|
+
│ └── index.js
|
|
384
|
+
├── utils/
|
|
385
|
+
├── App.jsx
|
|
386
|
+
└── main.jsx
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
**Barrel exports** (`index.js`): `export { LoginForm, SignupForm } from './components';`
|
|
390
|
+
|
|
391
|
+
## Conventions
|
|
392
|
+
|
|
393
|
+
### Naming
|
|
394
|
+
- Components: PascalCase (`UserProfile.jsx`)
|
|
395
|
+
- Hooks: camelCase with `use` prefix (`useDebounce.js`)
|
|
396
|
+
- Files: Match default export name (`UserProfile.jsx` exports `UserProfile`)
|
|
397
|
+
- Props: camelCase (`onClick`, `isLoading`, `userName`)
|
|
398
|
+
- Event handlers: `handle` prefix in component, `on` prefix in props (`<Button onClick={handleClick} />`)
|
|
399
|
+
|
|
400
|
+
### Formatting
|
|
401
|
+
- 2-space indentation
|
|
402
|
+
- Single quotes for strings
|
|
403
|
+
- Destructure props in function signature: `function Button({ onClick, children }) {}`
|
|
404
|
+
- Named exports for utilities, default export for components
|
|
405
|
+
|
|
406
|
+
### Component Structure Order
|
|
407
|
+
1. Imports
|
|
408
|
+
2. TypeScript types/interfaces (if using TS)
|
|
409
|
+
3. Component function
|
|
410
|
+
4. Styled components / CSS (if bottom of file)
|
|
411
|
+
5. Export
|
|
412
|
+
|
|
413
|
+
## Common Patterns
|
|
414
|
+
|
|
415
|
+
### 1. Optimistic Updates (TanStack Query)
|
|
416
|
+
```jsx
|
|
417
|
+
const mutation = useMutation(updateTodo, {
|
|
418
|
+
onMutate: async (newTodo) => {
|
|
419
|
+
await queryClient.cancelQueries(['todos']);
|
|
420
|
+
const prev = queryClient.getQueryData(['todos']);
|
|
421
|
+
queryClient.setQueryData(['todos'], old => [...old, newTodo]); // Optimistic
|
|
422
|
+
return { prev };
|
|
423
|
+
},
|
|
424
|
+
onError: (err, newTodo, context) => {
|
|
425
|
+
queryClient.setQueryData(['todos'], context.prev); // Rollback
|
|
426
|
+
},
|
|
427
|
+
onSettled: () => {
|
|
428
|
+
queryClient.invalidateQueries(['todos']); // Refetch
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### 2. Composition Over Prop Drilling
|
|
434
|
+
```jsx
|
|
435
|
+
// GOOD: Composition avoids prop drilling
|
|
436
|
+
function Dashboard() {
|
|
437
|
+
const user = useAuth();
|
|
438
|
+
return (
|
|
439
|
+
<Layout>
|
|
440
|
+
<Sidebar user={user} />
|
|
441
|
+
<Content user={user} />
|
|
442
|
+
</Layout>
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// BETTER: Layout provides user via Context
|
|
447
|
+
function Dashboard() {
|
|
448
|
+
return (
|
|
449
|
+
<Layout>
|
|
450
|
+
<Sidebar />
|
|
451
|
+
<Content />
|
|
452
|
+
</Layout>
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
### 3. Controlled vs Uncontrolled Forms
|
|
458
|
+
```jsx
|
|
459
|
+
// Controlled (React controls value)
|
|
460
|
+
function ControlledInput() {
|
|
461
|
+
const [value, setValue] = useState('');
|
|
462
|
+
return <input value={value} onChange={e => setValue(e.target.value)} />;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Uncontrolled (DOM controls value, use ref to read)
|
|
466
|
+
function UncontrolledInput() {
|
|
467
|
+
const inputRef = useRef();
|
|
468
|
+
const handleSubmit = () => console.log(inputRef.current.value);
|
|
469
|
+
return <input ref={inputRef} defaultValue="" />;
|
|
470
|
+
}
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
**Prefer controlled** for validation/formatting. Use uncontrolled for simple forms where you only read on submit.
|
|
474
|
+
|
|
475
|
+
### 4. Render Props (when hooks insufficient)
|
|
476
|
+
```jsx
|
|
477
|
+
function MouseTracker({ render }) {
|
|
478
|
+
const [pos, setPos] = useState({ x: 0, y: 0 });
|
|
479
|
+
useEffect(() => {
|
|
480
|
+
const handler = (e) => setPos({ x: e.clientX, y: e.clientY });
|
|
481
|
+
window.addEventListener('mousemove', handler);
|
|
482
|
+
return () => window.removeEventListener('mousemove', handler);
|
|
483
|
+
}, []);
|
|
484
|
+
return render(pos);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
<MouseTracker render={({ x, y }) => <div>Mouse at {x}, {y}</div>} />
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### 5. Code Splitting by Route
|
|
491
|
+
```jsx
|
|
492
|
+
const Home = lazy(() => import('./pages/Home'));
|
|
493
|
+
const About = lazy(() => import('./pages/About'));
|
|
494
|
+
|
|
495
|
+
<Suspense fallback={<Spinner />}>
|
|
496
|
+
<Routes>
|
|
497
|
+
<Route path="/" element={<Home />} />
|
|
498
|
+
<Route path="/about" element={<About />} />
|
|
499
|
+
</Routes>
|
|
500
|
+
</Suspense>
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
## Anti-Patterns
|
|
504
|
+
|
|
505
|
+
### 1. Prop Drilling
|
|
506
|
+
**Problem:** Passing props through 3+ intermediate components that don't use them.
|
|
507
|
+
|
|
508
|
+
**Solution:** Context for global data, composition for component-specific data.
|
|
509
|
+
|
|
510
|
+
```jsx
|
|
511
|
+
// BAD: Drilling user through Header -> Nav -> UserMenu
|
|
512
|
+
<Header user={user} />
|
|
513
|
+
<Nav user={user} />
|
|
514
|
+
<UserMenu user={user} />
|
|
515
|
+
|
|
516
|
+
// GOOD: Context provides user
|
|
517
|
+
<AuthProvider>
|
|
518
|
+
<Header />
|
|
519
|
+
</AuthProvider>
|
|
520
|
+
|
|
521
|
+
function UserMenu() {
|
|
522
|
+
const { user } = useContext(AuthContext);
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### 2. Derived State
|
|
527
|
+
**Problem:** Storing values in state that can be computed from existing state/props.
|
|
528
|
+
|
|
529
|
+
```jsx
|
|
530
|
+
// BAD: Storing derived value in state
|
|
531
|
+
function TodoList({ todos }) {
|
|
532
|
+
const [count, setCount] = useState(0);
|
|
533
|
+
useEffect(() => setCount(todos.length), [todos]); // Unnecessary state
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// GOOD: Compute on every render (cheap)
|
|
537
|
+
function TodoList({ todos }) {
|
|
538
|
+
const count = todos.length;
|
|
539
|
+
}
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
### 3. useEffect for Derived Values
|
|
543
|
+
**Problem:** Using useEffect to sync state instead of direct computation.
|
|
544
|
+
|
|
545
|
+
```jsx
|
|
546
|
+
// BAD: useEffect to derive fullName
|
|
547
|
+
const [firstName, setFirstName] = useState('');
|
|
548
|
+
const [lastName, setLastName] = useState('');
|
|
549
|
+
const [fullName, setFullName] = useState('');
|
|
550
|
+
useEffect(() => setFullName(`${firstName} ${lastName}`), [firstName, lastName]);
|
|
551
|
+
|
|
552
|
+
// GOOD: useMemo for expensive derivation (or just compute directly if cheap)
|
|
553
|
+
const fullName = useMemo(() => `${firstName} ${lastName}`, [firstName, lastName]);
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
### 4. Inline Object/Function Creation in JSX
|
|
557
|
+
**Problem:** Creates new reference every render, breaks React.memo.
|
|
558
|
+
|
|
559
|
+
```jsx
|
|
560
|
+
// BAD: New object every render
|
|
561
|
+
<UserProfile user={{ id: userId, name: userName }} />
|
|
562
|
+
|
|
563
|
+
// GOOD: useMemo for stable reference
|
|
564
|
+
const user = useMemo(() => ({ id: userId, name: userName }), [userId, userName]);
|
|
565
|
+
<UserProfile user={user} />
|
|
566
|
+
|
|
567
|
+
// BAD: New function every render
|
|
568
|
+
<button onClick={() => handleClick(id)}>Click</button>
|
|
569
|
+
|
|
570
|
+
// GOOD: useCallback for stable reference
|
|
571
|
+
const onClick = useCallback(() => handleClick(id), [id]);
|
|
572
|
+
<button onClick={onClick}>Click</button>
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
### 5. Mutating State Directly
|
|
576
|
+
**Problem:** React doesn't detect mutations, component won't re-render.
|
|
577
|
+
|
|
578
|
+
```jsx
|
|
579
|
+
// BAD: Mutating state
|
|
580
|
+
const [items, setItems] = useState([]);
|
|
581
|
+
items.push(newItem); // React doesn't detect this
|
|
582
|
+
setItems(items); // Won't trigger re-render
|
|
583
|
+
|
|
584
|
+
// GOOD: Immutable update
|
|
585
|
+
setItems([...items, newItem]);
|
|
586
|
+
|
|
587
|
+
// GOOD: Immer for complex nested updates
|
|
588
|
+
import { produce } from 'immer';
|
|
589
|
+
setItems(produce(draft => { draft.push(newItem); }));
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
## Integration Notes
|
|
593
|
+
|
|
594
|
+
### Multi-Session Coordination
|
|
595
|
+
|
|
596
|
+
**CRITICAL: Before starting work:**
|
|
597
|
+
1. Call `team_check_inbox` to read messages from orchestrator/teammates (ENFORCED: artifact tools blocked until you do this)
|
|
598
|
+
2. Read `shared-conventions` artifact for:
|
|
599
|
+
- Response format (e.g., `{ data: <result> }`)
|
|
600
|
+
- Error format (e.g., `{ error: <message> }`)
|
|
601
|
+
- Status codes (create=201, read=200, etc.)
|
|
602
|
+
- Enum values (EXACT strings, e.g., "pending" not "Pending")
|
|
603
|
+
- Date format (ISO 8601 vs Unix timestamps)
|
|
604
|
+
3. Read API contract artifact from backend worker (if exists):
|
|
605
|
+
- Endpoint paths (`/api/auth/login`)
|
|
606
|
+
- Request/response schemas
|
|
607
|
+
- Authentication requirements
|
|
608
|
+
|
|
609
|
+
**When done:**
|
|
610
|
+
1. Publish artifact with:
|
|
611
|
+
- `type: "frontend-structure"`
|
|
612
|
+
- `data: { routes: [...], components: [...], sharedTypes: [...], filesCreated: [...] }`
|
|
613
|
+
- File ownership is auto-registered from `filesCreated` array
|
|
614
|
+
2. Broadcast completion: `team_broadcast({ from: "frontend-dev", content: "Frontend routes complete. Published frontend-structure artifact." })`
|
|
615
|
+
|
|
616
|
+
**Communication:**
|
|
617
|
+
- Use `team_ask` to clarify API contract with backend worker (fallback only — orchestrator should provide contract upfront)
|
|
618
|
+
- Use `team_send_message` to notify backend if you need additional endpoints
|
|
619
|
+
|
|
620
|
+
**File Paths:**
|
|
621
|
+
- CRITICAL: Always use RELATIVE paths (`src/components/Button.jsx`), NEVER absolute (`C:\project\src\...`)
|
|
622
|
+
|
|
623
|
+
### Example Artifact
|
|
624
|
+
```json
|
|
625
|
+
{
|
|
626
|
+
"artifactId": "frontend-structure",
|
|
627
|
+
"type": "frontend-structure",
|
|
628
|
+
"name": "React Frontend Structure",
|
|
629
|
+
"data": {
|
|
630
|
+
"routes": [
|
|
631
|
+
{ "path": "/", "component": "Home", "protected": false },
|
|
632
|
+
{ "path": "/dashboard", "component": "Dashboard", "protected": true }
|
|
633
|
+
],
|
|
634
|
+
"components": [
|
|
635
|
+
{ "name": "Button", "path": "src/components/Button.jsx", "type": "atom" },
|
|
636
|
+
{ "name": "LoginForm", "path": "src/features/auth/components/LoginForm.jsx", "type": "organism" }
|
|
637
|
+
],
|
|
638
|
+
"sharedTypes": ["User", "AuthState"],
|
|
639
|
+
"filesCreated": [
|
|
640
|
+
"src/App.jsx",
|
|
641
|
+
"src/components/Button.jsx",
|
|
642
|
+
"src/features/auth/components/LoginForm.jsx"
|
|
643
|
+
]
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
### Backend API Contract Example (what to expect)
|
|
649
|
+
```json
|
|
650
|
+
{
|
|
651
|
+
"artifactId": "api-contract",
|
|
652
|
+
"data": {
|
|
653
|
+
"endpoints": [
|
|
654
|
+
{
|
|
655
|
+
"path": "/api/auth/login",
|
|
656
|
+
"method": "POST",
|
|
657
|
+
"request": { "email": "string", "password": "string" },
|
|
658
|
+
"response": { "data": { "token": "string", "user": { "id": "string", "email": "string" } } },
|
|
659
|
+
"errors": { "401": { "error": "Invalid credentials" } }
|
|
660
|
+
}
|
|
661
|
+
],
|
|
662
|
+
"authMethod": "Bearer token in Authorization header"
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
### Zustand Store Example (if needed)
|
|
668
|
+
```jsx
|
|
669
|
+
import create from 'zustand';
|
|
670
|
+
|
|
671
|
+
const useAuthStore = create((set) => ({
|
|
672
|
+
user: null,
|
|
673
|
+
token: null,
|
|
674
|
+
login: (user, token) => set({ user, token }),
|
|
675
|
+
logout: () => set({ user: null, token: null }),
|
|
676
|
+
}));
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
---
|
|
680
|
+
|
|
681
|
+
**Version:** 1.0.0
|
|
682
|
+
**Last Updated:** 2026-02-15
|
|
683
|
+
**Expertise Level:** Production-grade patterns for clearctx team workers
|