design-protocol 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/LICENSE +21 -0
- package/README.md +225 -0
- package/agents/dp-researcher.md +239 -0
- package/agents/dp-verifier.md +207 -0
- package/bin/install.js +464 -0
- package/commands/dp-back.md +221 -0
- package/commands/dp-discuss.md +257 -0
- package/commands/dp-execute.md +513 -0
- package/commands/dp-journey.md +85 -0
- package/commands/dp-progress.md +178 -0
- package/commands/dp-roadmap.md +83 -0
- package/commands/dp-skip.md +186 -0
- package/commands/dp-start.md +510 -0
- package/commands/dp-storytell.md +94 -0
- package/commands/dp-verify.md +207 -0
- package/package.json +59 -0
- package/skills/dp-color/SKILL.md +214 -0
- package/skills/dp-color/export_tokens.py +297 -0
- package/skills/dp-color/references/apca-contrast.md +87 -0
- package/skills/dp-color/references/hue-emotions.md +109 -0
- package/skills/dp-color/references/oklch-gamut.md +79 -0
- package/skills/dp-color/references/pitfalls.md +171 -0
- package/skills/dp-color/references/scale-patterns.md +206 -0
- package/skills/dp-color/references/tool-workflows.md +200 -0
- package/skills/dp-discovery/SKILL.md +480 -0
- package/skills/dp-eng_review/SKILL.md +471 -0
- package/skills/dp-eng_review/references/code-review-checklist.md +385 -0
- package/skills/dp-eng_review/references/react-patterns.md +512 -0
- package/skills/dp-eng_review/references/shadcn-patterns.md +510 -0
- package/skills/dp-eng_review/references/tailwind-conventions.md +351 -0
- package/skills/dp-journey/SKILL.md +682 -0
- package/skills/dp-journey/references/journey-types.md +97 -0
- package/skills/dp-journey/references/map-structures.md +177 -0
- package/skills/dp-journey/references/omnichannel-patterns.md +208 -0
- package/skills/dp-journey/references/research-methods.md +125 -0
- package/skills/dp-prd/SKILL.md +201 -0
- package/skills/dp-prd/references/claude-code-spec.md +107 -0
- package/skills/dp-prd/references/interview-questions.md +158 -0
- package/skills/dp-prd/references/section-templates.md +231 -0
- package/skills/dp-research/SKILL.md +540 -0
- package/skills/dp-research/references/facilitation-guide.md +291 -0
- package/skills/dp-research/references/interview-guide-template.md +190 -0
- package/skills/dp-research/references/method-selection.md +195 -0
- package/skills/dp-research/references/question-writing.md +244 -0
- package/skills/dp-research/references/research-report-template.md +363 -0
- package/skills/dp-research/references/synthesis-methods.md +289 -0
- package/skills/dp-research/references/usability-test-template.md +260 -0
- package/skills/dp-roadmap/SKILL.md +648 -0
- package/skills/dp-roadmap/references/prioritization-frameworks.md +312 -0
- package/skills/dp-roadmap/references/roadmap-structures.md +179 -0
- package/skills/dp-roadmap/references/roadmap-workshops.md +264 -0
- package/skills/dp-roadmap/references/theme-development.md +168 -0
- package/skills/dp-storytell/SKILL.md +645 -0
- package/skills/dp-storytell/references/audience-playbooks.md +260 -0
- package/skills/dp-storytell/references/content-type-templates.md +310 -0
- package/skills/dp-storytell/references/delivery-tactics.md +228 -0
- package/skills/dp-storytell/references/narrative-frameworks.md +259 -0
- package/skills/dp-ui/SKILL.md +503 -0
- package/skills/dp-ui/references/b2b-enterprise-patterns.md +319 -0
- package/skills/dp-ui/references/data-visualization.md +304 -0
- package/skills/dp-ui/references/visual-design-principles.md +237 -0
- package/skills/dp-ux/SKILL.md +414 -0
- package/skills/dp-ux/references/accessibility-checklist.md +128 -0
- package/skills/dp-ux/references/product-excellence.md +149 -0
- package/skills/dp-ux/references/usability-principles.md +140 -0
- package/skills/dp-ux/references/ux-patterns.md +221 -0
- package/templates/config.json +55 -0
- package/templates/context.md +96 -0
- package/templates/project.md +83 -0
- package/templates/requirements.md +137 -0
- package/templates/roadmap.md +168 -0
- package/templates/state.md +107 -0
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
# React 19 Patterns
|
|
2
|
+
|
|
3
|
+
Best practices and anti-patterns for React 19 in Next.js 16. Reference this when reviewing component implementations.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Component Structure
|
|
8
|
+
|
|
9
|
+
### Function Components (Standard)
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
// GOOD: Standard function component
|
|
13
|
+
interface UserCardProps {
|
|
14
|
+
user: User
|
|
15
|
+
onEdit?: (id: string) => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function UserCard({ user, onEdit }: UserCardProps) {
|
|
19
|
+
return (
|
|
20
|
+
<Card>
|
|
21
|
+
<CardHeader>
|
|
22
|
+
<CardTitle>{user.name}</CardTitle>
|
|
23
|
+
</CardHeader>
|
|
24
|
+
</Card>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Props Typing
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
// GOOD: Explicit interface
|
|
33
|
+
interface ButtonProps {
|
|
34
|
+
variant?: 'primary' | 'secondary'
|
|
35
|
+
size?: 'sm' | 'md' | 'lg'
|
|
36
|
+
isLoading?: boolean
|
|
37
|
+
children: React.ReactNode
|
|
38
|
+
onClick?: () => void
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// GOOD: Extending HTML attributes
|
|
42
|
+
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
43
|
+
variant?: 'primary' | 'secondary'
|
|
44
|
+
isLoading?: boolean
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// GOOD: Using ComponentProps for composition
|
|
48
|
+
import { ComponentProps } from 'react'
|
|
49
|
+
type ButtonProps = ComponentProps<typeof Button> & {
|
|
50
|
+
customProp?: string
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Lists and Keys
|
|
57
|
+
|
|
58
|
+
### Always Provide Stable Keys
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
// BAD: Using index as key
|
|
62
|
+
{items.map((item, index) => (
|
|
63
|
+
<Item key={index} {...item} /> // Will cause issues with reordering
|
|
64
|
+
))}
|
|
65
|
+
|
|
66
|
+
// BAD: No key
|
|
67
|
+
{items.map((item) => (
|
|
68
|
+
<Item {...item} /> // React will warn
|
|
69
|
+
))}
|
|
70
|
+
|
|
71
|
+
// GOOD: Stable unique key
|
|
72
|
+
{items.map((item) => (
|
|
73
|
+
<Item key={item.id} {...item} />
|
|
74
|
+
))}
|
|
75
|
+
|
|
76
|
+
// GOOD: Compound key when needed
|
|
77
|
+
{items.map((item) => (
|
|
78
|
+
<Item key={`${item.category}-${item.id}`} {...item} />
|
|
79
|
+
))}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Event Handlers & Callbacks {#callbacks}
|
|
85
|
+
|
|
86
|
+
### Avoid Inline Functions in Render
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
// BAD: Creates new function every render
|
|
90
|
+
<Button onClick={() => handleDelete(item.id)}>Delete</Button>
|
|
91
|
+
|
|
92
|
+
// GOOD: useCallback for stable reference
|
|
93
|
+
const handleItemDelete = useCallback((id: string) => {
|
|
94
|
+
deleteItem(id)
|
|
95
|
+
}, [deleteItem])
|
|
96
|
+
|
|
97
|
+
// Then in render
|
|
98
|
+
<Button onClick={() => handleItemDelete(item.id)}>Delete</Button>
|
|
99
|
+
|
|
100
|
+
// BETTER: Pass data via data attributes
|
|
101
|
+
const handleDelete = useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
|
|
102
|
+
const id = e.currentTarget.dataset.itemId
|
|
103
|
+
if (id) deleteItem(id)
|
|
104
|
+
}, [deleteItem])
|
|
105
|
+
|
|
106
|
+
<Button data-item-id={item.id} onClick={handleDelete}>Delete</Button>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Event Handler Typing
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
// GOOD: Properly typed handlers
|
|
113
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
114
|
+
setValue(e.target.value)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
|
118
|
+
e.preventDefault()
|
|
119
|
+
// submit logic
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
123
|
+
if (e.key === 'Enter') {
|
|
124
|
+
submit()
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## State Management
|
|
132
|
+
|
|
133
|
+
### Derived State
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
// BAD: Duplicating state
|
|
137
|
+
const [items, setItems] = useState<Item[]>([])
|
|
138
|
+
const [filteredItems, setFilteredItems] = useState<Item[]>([]) // Derived!
|
|
139
|
+
const [itemCount, setItemCount] = useState(0) // Derived!
|
|
140
|
+
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
setFilteredItems(items.filter(i => i.active))
|
|
143
|
+
setItemCount(items.length)
|
|
144
|
+
}, [items])
|
|
145
|
+
|
|
146
|
+
// GOOD: Derive values directly
|
|
147
|
+
const [items, setItems] = useState<Item[]>([])
|
|
148
|
+
const filteredItems = items.filter(i => i.active) // Computed
|
|
149
|
+
const itemCount = items.length // Computed
|
|
150
|
+
|
|
151
|
+
// GOOD: useMemo for expensive derivations
|
|
152
|
+
const filteredItems = useMemo(
|
|
153
|
+
() => items.filter(i => expensiveCheck(i)),
|
|
154
|
+
[items]
|
|
155
|
+
)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### State Initialization
|
|
159
|
+
|
|
160
|
+
```tsx
|
|
161
|
+
// BAD: Expensive computation on every render
|
|
162
|
+
const [data, setData] = useState(expensiveComputation())
|
|
163
|
+
|
|
164
|
+
// GOOD: Lazy initialization
|
|
165
|
+
const [data, setData] = useState(() => expensiveComputation())
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### State Updates
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
// BAD: State based on previous without using callback
|
|
172
|
+
const increment = () => {
|
|
173
|
+
setCount(count + 1) // Might be stale
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// GOOD: Functional update for state based on previous
|
|
177
|
+
const increment = () => {
|
|
178
|
+
setCount(prev => prev + 1)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// BAD: Multiple related state updates
|
|
182
|
+
const [firstName, setFirstName] = useState('')
|
|
183
|
+
const [lastName, setLastName] = useState('')
|
|
184
|
+
const [email, setEmail] = useState('')
|
|
185
|
+
|
|
186
|
+
// GOOD: Group related state
|
|
187
|
+
const [formData, setFormData] = useState({
|
|
188
|
+
firstName: '',
|
|
189
|
+
lastName: '',
|
|
190
|
+
email: '',
|
|
191
|
+
})
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## useEffect Patterns
|
|
197
|
+
|
|
198
|
+
### Dependency Arrays
|
|
199
|
+
|
|
200
|
+
```tsx
|
|
201
|
+
// BAD: Missing dependencies
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
fetchUser(userId) // userId should be in deps
|
|
204
|
+
}, [])
|
|
205
|
+
|
|
206
|
+
// BAD: Object/array in deps (new reference each render)
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
doSomething(options)
|
|
209
|
+
}, [options]) // If options = { foo: 'bar' } in render, runs every time
|
|
210
|
+
|
|
211
|
+
// GOOD: Stable primitive dependencies
|
|
212
|
+
useEffect(() => {
|
|
213
|
+
fetchUser(userId)
|
|
214
|
+
}, [userId])
|
|
215
|
+
|
|
216
|
+
// GOOD: Memoize objects if needed in deps
|
|
217
|
+
const options = useMemo(() => ({ foo: 'bar' }), [])
|
|
218
|
+
useEffect(() => {
|
|
219
|
+
doSomething(options)
|
|
220
|
+
}, [options])
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Cleanup
|
|
224
|
+
|
|
225
|
+
```tsx
|
|
226
|
+
// BAD: No cleanup for subscriptions
|
|
227
|
+
useEffect(() => {
|
|
228
|
+
const subscription = subscribe(channel)
|
|
229
|
+
// Memory leak! No cleanup
|
|
230
|
+
}, [channel])
|
|
231
|
+
|
|
232
|
+
// GOOD: Proper cleanup
|
|
233
|
+
useEffect(() => {
|
|
234
|
+
const subscription = subscribe(channel)
|
|
235
|
+
return () => {
|
|
236
|
+
subscription.unsubscribe()
|
|
237
|
+
}
|
|
238
|
+
}, [channel])
|
|
239
|
+
|
|
240
|
+
// GOOD: Abort controller for fetch
|
|
241
|
+
useEffect(() => {
|
|
242
|
+
const controller = new AbortController()
|
|
243
|
+
|
|
244
|
+
fetch(url, { signal: controller.signal })
|
|
245
|
+
.then(res => res.json())
|
|
246
|
+
.then(setData)
|
|
247
|
+
.catch(err => {
|
|
248
|
+
if (err.name !== 'AbortError') {
|
|
249
|
+
setError(err)
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
return () => controller.abort()
|
|
254
|
+
}, [url])
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Avoid useEffect for Derived Data
|
|
258
|
+
|
|
259
|
+
```tsx
|
|
260
|
+
// BAD: useEffect for transformation
|
|
261
|
+
const [items, setItems] = useState<Item[]>([])
|
|
262
|
+
const [sortedItems, setSortedItems] = useState<Item[]>([])
|
|
263
|
+
|
|
264
|
+
useEffect(() => {
|
|
265
|
+
setSortedItems([...items].sort((a, b) => a.name.localeCompare(b.name)))
|
|
266
|
+
}, [items])
|
|
267
|
+
|
|
268
|
+
// GOOD: Compute directly or useMemo
|
|
269
|
+
const sortedItems = useMemo(
|
|
270
|
+
() => [...items].sort((a, b) => a.name.localeCompare(b.name)),
|
|
271
|
+
[items]
|
|
272
|
+
)
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## Performance Optimization
|
|
278
|
+
|
|
279
|
+
### When to Use useMemo
|
|
280
|
+
|
|
281
|
+
```tsx
|
|
282
|
+
// DON'T: Simple operations
|
|
283
|
+
const fullName = useMemo(() => `${first} ${last}`, [first, last]) // Overkill
|
|
284
|
+
|
|
285
|
+
// DO: Expensive computations
|
|
286
|
+
const sortedData = useMemo(
|
|
287
|
+
() => data.sort((a, b) => complexComparison(a, b)),
|
|
288
|
+
[data]
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
// DO: Reference stability for child components
|
|
292
|
+
const chartOptions = useMemo(
|
|
293
|
+
() => ({ responsive: true, scales: { ... } }),
|
|
294
|
+
[]
|
|
295
|
+
)
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### When to Use useCallback
|
|
299
|
+
|
|
300
|
+
```tsx
|
|
301
|
+
// DON'T: Handlers that don't get passed down
|
|
302
|
+
const handleClick = useCallback(() => {
|
|
303
|
+
doSomething()
|
|
304
|
+
}, [])
|
|
305
|
+
// If handleClick isn't passed to memoized children, useCallback is pointless
|
|
306
|
+
|
|
307
|
+
// DO: Handlers passed to memoized children
|
|
308
|
+
const handleDelete = useCallback((id: string) => {
|
|
309
|
+
deleteItem(id)
|
|
310
|
+
}, [deleteItem])
|
|
311
|
+
|
|
312
|
+
<MemoizedList onDelete={handleDelete} />
|
|
313
|
+
|
|
314
|
+
// DO: Handlers in dependency arrays
|
|
315
|
+
const handleFetch = useCallback(() => {
|
|
316
|
+
fetch(url)
|
|
317
|
+
}, [url])
|
|
318
|
+
|
|
319
|
+
useEffect(() => {
|
|
320
|
+
handleFetch()
|
|
321
|
+
}, [handleFetch])
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### React.memo
|
|
325
|
+
|
|
326
|
+
```tsx
|
|
327
|
+
// Use for components that:
|
|
328
|
+
// 1. Render often
|
|
329
|
+
// 2. Receive the same props usually
|
|
330
|
+
// 3. Are expensive to render
|
|
331
|
+
|
|
332
|
+
const ExpensiveList = memo(function ExpensiveList({ items }: Props) {
|
|
333
|
+
return (
|
|
334
|
+
<ul>
|
|
335
|
+
{items.map(item => (
|
|
336
|
+
<ExpensiveItem key={item.id} {...item} />
|
|
337
|
+
))}
|
|
338
|
+
</ul>
|
|
339
|
+
)
|
|
340
|
+
})
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## React 19 Specific Patterns
|
|
346
|
+
|
|
347
|
+
### use() Hook
|
|
348
|
+
|
|
349
|
+
```tsx
|
|
350
|
+
// React 19: use() for promises in render
|
|
351
|
+
import { use } from 'react'
|
|
352
|
+
|
|
353
|
+
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
|
|
354
|
+
const user = use(userPromise) // Suspends until resolved
|
|
355
|
+
return <div>{user.name}</div>
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Actions (Server Actions in Next.js)
|
|
360
|
+
|
|
361
|
+
```tsx
|
|
362
|
+
// Form with Server Action
|
|
363
|
+
async function createItem(formData: FormData) {
|
|
364
|
+
'use server'
|
|
365
|
+
const name = formData.get('name')
|
|
366
|
+
// Create item...
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function CreateForm() {
|
|
370
|
+
return (
|
|
371
|
+
<form action={createItem}>
|
|
372
|
+
<Input name="name" />
|
|
373
|
+
<Button type="submit">Create</Button>
|
|
374
|
+
</form>
|
|
375
|
+
)
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### useActionState (React 19)
|
|
380
|
+
|
|
381
|
+
```tsx
|
|
382
|
+
import { useActionState } from 'react'
|
|
383
|
+
|
|
384
|
+
function Form() {
|
|
385
|
+
const [state, formAction, isPending] = useActionState(
|
|
386
|
+
async (prevState, formData) => {
|
|
387
|
+
const result = await submitForm(formData)
|
|
388
|
+
return result
|
|
389
|
+
},
|
|
390
|
+
null
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
return (
|
|
394
|
+
<form action={formAction}>
|
|
395
|
+
<Input name="email" />
|
|
396
|
+
<Button disabled={isPending}>
|
|
397
|
+
{isPending ? 'Submitting...' : 'Submit'}
|
|
398
|
+
</Button>
|
|
399
|
+
{state?.error && <p className="text-destructive">{state.error}</p>}
|
|
400
|
+
</form>
|
|
401
|
+
)
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
### useOptimistic (React 19)
|
|
406
|
+
|
|
407
|
+
```tsx
|
|
408
|
+
import { useOptimistic } from 'react'
|
|
409
|
+
|
|
410
|
+
function TodoList({ todos }: { todos: Todo[] }) {
|
|
411
|
+
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
|
|
412
|
+
todos,
|
|
413
|
+
(state, newTodo: Todo) => [...state, newTodo]
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
async function addTodo(formData: FormData) {
|
|
417
|
+
const newTodo = { id: crypto.randomUUID(), text: formData.get('text') }
|
|
418
|
+
addOptimisticTodo(newTodo) // Immediately show
|
|
419
|
+
await createTodo(formData) // Actually create
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return (
|
|
423
|
+
<form action={addTodo}>
|
|
424
|
+
{/* ... */}
|
|
425
|
+
</form>
|
|
426
|
+
)
|
|
427
|
+
}
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
## Common Anti-Patterns
|
|
433
|
+
|
|
434
|
+
### Prop Drilling
|
|
435
|
+
|
|
436
|
+
```tsx
|
|
437
|
+
// BAD: Passing props through many layers
|
|
438
|
+
<App user={user} />
|
|
439
|
+
<Layout user={user} />
|
|
440
|
+
<Sidebar user={user} />
|
|
441
|
+
<UserMenu user={user} />
|
|
442
|
+
|
|
443
|
+
// GOOD: Context for widely-used data
|
|
444
|
+
const UserContext = createContext<User | null>(null)
|
|
445
|
+
|
|
446
|
+
function App() {
|
|
447
|
+
return (
|
|
448
|
+
<UserContext.Provider value={user}>
|
|
449
|
+
<Layout />
|
|
450
|
+
</UserContext.Provider>
|
|
451
|
+
)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function UserMenu() {
|
|
455
|
+
const user = useContext(UserContext)
|
|
456
|
+
// ...
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### Unnecessary State
|
|
461
|
+
|
|
462
|
+
```tsx
|
|
463
|
+
// BAD: State for URL params (Next.js handles this)
|
|
464
|
+
const [searchQuery, setSearchQuery] = useState(
|
|
465
|
+
searchParams.get('q') || ''
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
// GOOD: Use URL directly
|
|
469
|
+
const searchQuery = searchParams.get('q') || ''
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
### Conditional Hooks
|
|
473
|
+
|
|
474
|
+
```tsx
|
|
475
|
+
// BAD: Hooks called conditionally
|
|
476
|
+
if (isLoggedIn) {
|
|
477
|
+
const user = useUser() // BREAKS RULES OF HOOKS
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// GOOD: Always call, handle null
|
|
481
|
+
const user = useUser() // Always called
|
|
482
|
+
if (!isLoggedIn || !user) return <Login />
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
---
|
|
486
|
+
|
|
487
|
+
## Testing Considerations
|
|
488
|
+
|
|
489
|
+
### Testable Component Structure
|
|
490
|
+
|
|
491
|
+
```tsx
|
|
492
|
+
// GOOD: Logic separated from UI
|
|
493
|
+
function useCounter(initial = 0) {
|
|
494
|
+
const [count, setCount] = useState(initial)
|
|
495
|
+
const increment = useCallback(() => setCount(c => c + 1), [])
|
|
496
|
+
const decrement = useCallback(() => setCount(c => c - 1), [])
|
|
497
|
+
return { count, increment, decrement }
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function Counter() {
|
|
501
|
+
const { count, increment, decrement } = useCounter()
|
|
502
|
+
return (
|
|
503
|
+
<div>
|
|
504
|
+
<Button onClick={decrement}>-</Button>
|
|
505
|
+
<span>{count}</span>
|
|
506
|
+
<Button onClick={increment}>+</Button>
|
|
507
|
+
</div>
|
|
508
|
+
)
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Hook can be tested separately from component
|
|
512
|
+
```
|