@codihaus/claude-skills 1.6.18 → 1.6.20
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/knowledge/stacks/_index.md +1 -0
- package/knowledge/stacks/nextjs/_index.md +32 -0
- package/knowledge/stacks/nextjs/references/rsc-patterns.md +705 -0
- package/knowledge/stacks/react/_index.md +21 -0
- package/knowledge/stacks/react/references/performance.md +573 -0
- package/knowledge/stacks/vue/_index.md +750 -0
- package/package.json +1 -1
- package/skills/_registry.md +1 -0
- package/skills/dev-coding/SKILL.md +4 -1
- package/skills/dev-review/SKILL.md +11 -1
|
@@ -571,9 +571,30 @@ When reviewing React code:
|
|
|
571
571
|
- useDeferredValue (defer updates for performance)
|
|
572
572
|
- useSyncExternalStore (external store integration)
|
|
573
573
|
|
|
574
|
+
## Advanced Performance Patterns
|
|
575
|
+
|
|
576
|
+
**See `references/performance.md` for comprehensive optimization guide covering**:
|
|
577
|
+
- Re-render optimization (functional setState, lazy initialization, transitions)
|
|
578
|
+
- Rendering performance (hoisting, content-visibility, hydration)
|
|
579
|
+
- Client-side optimization (passive listeners, storage caching)
|
|
580
|
+
- JavaScript performance (index maps, Set/Map, toSorted)
|
|
581
|
+
- Advanced patterns (useLatest, event handler refs)
|
|
582
|
+
|
|
583
|
+
**Key takeaways**:
|
|
584
|
+
- Use functional setState to prevent stale closures
|
|
585
|
+
- Lazy initialize expensive state with function
|
|
586
|
+
- Mark non-urgent updates with startTransition
|
|
587
|
+
- Hoist static JSX outside render
|
|
588
|
+
- Use content-visibility for long lists
|
|
589
|
+
- Cache storage API calls in memory
|
|
590
|
+
- Build index Maps for repeated lookups
|
|
591
|
+
- Use toSorted() instead of sort() for immutability
|
|
592
|
+
|
|
574
593
|
## Resources
|
|
575
594
|
|
|
576
595
|
- [React Docs](https://react.dev) - Official documentation
|
|
577
596
|
- [React DevTools](https://react.dev/learn/react-developer-tools) - Browser extension
|
|
578
597
|
- [Rules of Hooks](https://react.dev/reference/rules/rules-of-hooks) - Hook constraints
|
|
579
598
|
- [React TypeScript Cheatsheet](https://react-typescript-cheatsheet.netlify.app) - TS patterns
|
|
599
|
+
- [Vercel React Best Practices](https://github.com/vercel/react-best-practices) - Performance patterns
|
|
600
|
+
- `references/performance.md` - Detailed optimization guide
|
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
# React Performance Optimization
|
|
2
|
+
|
|
3
|
+
> Critical performance patterns from Vercel Engineering
|
|
4
|
+
|
|
5
|
+
## Re-render Optimization
|
|
6
|
+
|
|
7
|
+
### Functional setState Updates
|
|
8
|
+
|
|
9
|
+
**Impact**: Prevents stale closures, enables stable callbacks
|
|
10
|
+
|
|
11
|
+
Always use functional updates when state depends on current state:
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
// ❌ Incorrect: stale closure risk
|
|
15
|
+
const removeItem = useCallback((id: string) => {
|
|
16
|
+
setItems(items.filter(item => item.id !== id))
|
|
17
|
+
}, []) // Missing items dependency - STALE
|
|
18
|
+
|
|
19
|
+
// ✅ Correct: always uses latest state
|
|
20
|
+
const removeItem = useCallback((id: string) => {
|
|
21
|
+
setItems(curr => curr.filter(item => item.id !== id))
|
|
22
|
+
}, []) // Safe and stable
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Lazy State Initialization
|
|
26
|
+
|
|
27
|
+
**Impact**: Expensive initial computation runs only once
|
|
28
|
+
|
|
29
|
+
Pass function to `useState` for expensive initial values:
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
// ❌ Incorrect: runs on every render
|
|
33
|
+
const [settings, setSettings] = useState(
|
|
34
|
+
JSON.parse(localStorage.getItem('settings') || '{}')
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
// ✅ Correct: runs only once
|
|
38
|
+
const [settings, setSettings] = useState(() => {
|
|
39
|
+
const stored = localStorage.getItem('settings')
|
|
40
|
+
return stored ? JSON.parse(stored) : {}
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Use Transitions for Non-Urgent Updates
|
|
45
|
+
|
|
46
|
+
**Impact**: Maintains UI responsiveness
|
|
47
|
+
|
|
48
|
+
Mark frequent, non-urgent updates as transitions:
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { useTransition } from 'react'
|
|
52
|
+
|
|
53
|
+
function SearchResults() {
|
|
54
|
+
const [isPending, startTransition] = useTransition()
|
|
55
|
+
const [results, setResults] = useState([])
|
|
56
|
+
|
|
57
|
+
function handleSearch(query: string) {
|
|
58
|
+
startTransition(() => {
|
|
59
|
+
// Non-urgent: heavy filtering/search
|
|
60
|
+
setResults(expensiveSearch(query))
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<>
|
|
66
|
+
<input onChange={e => handleSearch(e.target.value)} />
|
|
67
|
+
{isPending && <Spinner />}
|
|
68
|
+
<Results data={results} />
|
|
69
|
+
</>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Defer State Reads to Usage Point
|
|
75
|
+
|
|
76
|
+
**Impact**: Avoids unnecessary re-renders
|
|
77
|
+
|
|
78
|
+
Don't subscribe to state if only read in callbacks:
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
// ❌ Incorrect: component re-renders on every URL change
|
|
82
|
+
function Component() {
|
|
83
|
+
const searchParams = useSearchParams() // Re-renders on change
|
|
84
|
+
|
|
85
|
+
const handleSubmit = () => {
|
|
86
|
+
const query = searchParams.get('q') // Used only here
|
|
87
|
+
submitForm(query)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return <button onClick={handleSubmit}>Submit</button>
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ✅ Correct: no re-renders, read directly in callback
|
|
94
|
+
function Component() {
|
|
95
|
+
const handleSubmit = () => {
|
|
96
|
+
const query = new URLSearchParams(window.location.search).get('q')
|
|
97
|
+
submitForm(query)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return <button onClick={handleSubmit}>Submit</button>
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Extract to Memoized Components
|
|
105
|
+
|
|
106
|
+
**Impact**: Prevents unnecessary re-renders of expensive subtrees
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
// ❌ Incorrect: entire list re-renders on count change
|
|
110
|
+
function Page() {
|
|
111
|
+
const [count, setCount] = useState(0)
|
|
112
|
+
return (
|
|
113
|
+
<div>
|
|
114
|
+
<button onClick={() => setCount(c => c + 1)}>{count}</button>
|
|
115
|
+
<ExpensiveList items={items} /> {/* Re-renders unnecessarily */}
|
|
116
|
+
</div>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ✅ Correct: list doesn't re-render
|
|
121
|
+
const MemoizedList = memo(ExpensiveList)
|
|
122
|
+
|
|
123
|
+
function Page() {
|
|
124
|
+
const [count, setCount] = useState(0)
|
|
125
|
+
return (
|
|
126
|
+
<div>
|
|
127
|
+
<button onClick={() => setCount(c => c + 1)}>{count}</button>
|
|
128
|
+
<MemoizedList items={items} />
|
|
129
|
+
</div>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Narrow Effect Dependencies
|
|
135
|
+
|
|
136
|
+
**Impact**: Reduces effect re-runs
|
|
137
|
+
|
|
138
|
+
Extract only needed properties instead of whole objects:
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
// ❌ Incorrect: effect runs on any user change
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
logUserActivity(user.id)
|
|
144
|
+
}, [user]) // user object changes often
|
|
145
|
+
|
|
146
|
+
// ✅ Correct: effect only runs when ID changes
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
logUserActivity(user.id)
|
|
149
|
+
}, [user.id]) // Stable value
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Subscribe to Derived State
|
|
153
|
+
|
|
154
|
+
**Impact**: Avoids storing and synchronizing computed values
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
// ❌ Incorrect: derived state in useState (sync issue)
|
|
158
|
+
const [items, setItems] = useState([])
|
|
159
|
+
const [count, setCount] = useState(0)
|
|
160
|
+
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
setCount(items.length) // Sync issue!
|
|
163
|
+
}, [items])
|
|
164
|
+
|
|
165
|
+
// ✅ Correct: compute during render
|
|
166
|
+
const [items, setItems] = useState([])
|
|
167
|
+
const count = items.length // Always in sync
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Rendering Performance
|
|
171
|
+
|
|
172
|
+
### Hoist Static JSX Elements
|
|
173
|
+
|
|
174
|
+
**Impact**: Prevents recreation on every render
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
// ❌ Incorrect: creates new object on every render
|
|
178
|
+
function Component({ title }) {
|
|
179
|
+
return (
|
|
180
|
+
<div>
|
|
181
|
+
<Icon name="check" color="green" /> {/* New object every render */}
|
|
182
|
+
{title}
|
|
183
|
+
</div>
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ✅ Correct: created once
|
|
188
|
+
const CheckIcon = <Icon name="check" color="green" />
|
|
189
|
+
|
|
190
|
+
function Component({ title }) {
|
|
191
|
+
return (
|
|
192
|
+
<div>
|
|
193
|
+
{CheckIcon}
|
|
194
|
+
{title}
|
|
195
|
+
</div>
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### CSS content-visibility for Long Lists
|
|
201
|
+
|
|
202
|
+
**Impact**: 10× faster initial render
|
|
203
|
+
|
|
204
|
+
Browser skips layout/paint for off-screen items:
|
|
205
|
+
|
|
206
|
+
```css
|
|
207
|
+
.message-item {
|
|
208
|
+
content-visibility: auto;
|
|
209
|
+
contain-intrinsic-size: 0 80px; /* Estimated height */
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Use Explicit Conditional Rendering
|
|
214
|
+
|
|
215
|
+
**Impact**: Prevents rendering unwanted values
|
|
216
|
+
|
|
217
|
+
Use ternary when condition can be `0` or `NaN`:
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
// ❌ Incorrect: renders "0" when count is 0
|
|
221
|
+
{count && <div>{count} items</div>}
|
|
222
|
+
|
|
223
|
+
// ✅ Correct: renders nothing when 0
|
|
224
|
+
{count > 0 && <div>{count} items</div>}
|
|
225
|
+
// or
|
|
226
|
+
{count ? <div>{count} items</div> : null}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Prevent Hydration Mismatch Without Flickering
|
|
230
|
+
|
|
231
|
+
**Impact**: No FOUC (Flash of Unstyled Content)
|
|
232
|
+
|
|
233
|
+
Use synchronous inline script:
|
|
234
|
+
|
|
235
|
+
```tsx
|
|
236
|
+
function ThemeWrapper({ children }) {
|
|
237
|
+
return (
|
|
238
|
+
<>
|
|
239
|
+
<div id="theme-wrapper">{children}</div>
|
|
240
|
+
<script dangerouslySetInnerHTML={{__html: `
|
|
241
|
+
(function() {
|
|
242
|
+
try {
|
|
243
|
+
var theme = localStorage.getItem('theme') || 'light';
|
|
244
|
+
var el = document.getElementById('theme-wrapper');
|
|
245
|
+
if (el) el.className = theme;
|
|
246
|
+
} catch (e) {}
|
|
247
|
+
})();
|
|
248
|
+
`}} />
|
|
249
|
+
</>
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Animate SVG Wrapper Instead of Element
|
|
255
|
+
|
|
256
|
+
**Impact**: Reduces paint complexity
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
// ❌ Incorrect: animates SVG element directly
|
|
260
|
+
<svg style={{ transform: `translateX(${x}px)` }}>
|
|
261
|
+
<path d="..." />
|
|
262
|
+
</svg>
|
|
263
|
+
|
|
264
|
+
// ✅ Correct: animate wrapper div
|
|
265
|
+
<div style={{ transform: `translateX(${x}px)` }}>
|
|
266
|
+
<svg><path d="..." /></svg>
|
|
267
|
+
</div>
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Activity Component for Show/Hide
|
|
271
|
+
|
|
272
|
+
**Impact**: Prevents layout thrashing
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
// ❌ Incorrect: removes from DOM (layout thrash)
|
|
276
|
+
{isVisible && <ExpensiveComponent />}
|
|
277
|
+
|
|
278
|
+
// ✅ Correct: uses visibility (keeps layout)
|
|
279
|
+
<div style={{ visibility: isVisible ? 'visible' : 'hidden' }}>
|
|
280
|
+
<ExpensiveComponent />
|
|
281
|
+
</div>
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Client-Side Optimization
|
|
285
|
+
|
|
286
|
+
### Use Passive Event Listeners
|
|
287
|
+
|
|
288
|
+
**Impact**: Eliminates scroll delay
|
|
289
|
+
|
|
290
|
+
Add `{ passive: true }` to touch/wheel events:
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
useEffect(() => {
|
|
294
|
+
const handleScroll = (e: Event) => {
|
|
295
|
+
// No preventDefault() call
|
|
296
|
+
updateScrollPosition()
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ✅ Passive listener - doesn't block scrolling
|
|
300
|
+
window.addEventListener('scroll', handleScroll, { passive: true })
|
|
301
|
+
|
|
302
|
+
return () => {
|
|
303
|
+
window.removeEventListener('scroll', handleScroll, { passive: true } as any)
|
|
304
|
+
}
|
|
305
|
+
}, [])
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Deduplicate Global Event Listeners
|
|
309
|
+
|
|
310
|
+
**Impact**: N instances = 1 listener instead of N
|
|
311
|
+
|
|
312
|
+
Use shared hook for global events:
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
// ❌ Incorrect: N listeners for N components
|
|
316
|
+
function Component() {
|
|
317
|
+
useEffect(() => {
|
|
318
|
+
const handler = () => console.log('resize')
|
|
319
|
+
window.addEventListener('resize', handler)
|
|
320
|
+
return () => window.removeEventListener('resize', handler)
|
|
321
|
+
}, [])
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ✅ Correct: 1 listener shared across all instances
|
|
325
|
+
import { useSWRSubscription } from 'swr/subscription'
|
|
326
|
+
|
|
327
|
+
function useWindowSize() {
|
|
328
|
+
return useSWRSubscription('window-size', (key, { next }) => {
|
|
329
|
+
const handler = () => next(null, {
|
|
330
|
+
width: window.innerWidth,
|
|
331
|
+
height: window.innerHeight
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
window.addEventListener('resize', handler)
|
|
335
|
+
handler() // Initial value
|
|
336
|
+
|
|
337
|
+
return () => window.removeEventListener('resize', handler)
|
|
338
|
+
})
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Cache Storage API Calls
|
|
343
|
+
|
|
344
|
+
**Impact**: Avoids synchronous I/O on every access
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
// ❌ Incorrect: reads from localStorage on every access
|
|
348
|
+
function useSettings() {
|
|
349
|
+
const [settings, setSettings] = useState(() => {
|
|
350
|
+
return JSON.parse(localStorage.getItem('settings') || '{}')
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
// Every time this runs, reads from localStorage
|
|
354
|
+
const value = localStorage.getItem('key')
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ✅ Correct: cache in memory
|
|
358
|
+
const settingsCache = new Map<string, any>()
|
|
359
|
+
|
|
360
|
+
function getCachedSetting(key: string) {
|
|
361
|
+
if (settingsCache.has(key)) {
|
|
362
|
+
return settingsCache.get(key)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const value = localStorage.getItem(key)
|
|
366
|
+
const parsed = value ? JSON.parse(value) : null
|
|
367
|
+
settingsCache.set(key, parsed)
|
|
368
|
+
return parsed
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Invalidate on storage events
|
|
372
|
+
window.addEventListener('storage', (e) => {
|
|
373
|
+
if (e.key) settingsCache.delete(e.key)
|
|
374
|
+
})
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
## JavaScript Performance
|
|
378
|
+
|
|
379
|
+
### Build Index Maps for Repeated Lookups
|
|
380
|
+
|
|
381
|
+
**Impact**: O(n×m) → O(n+m), 1M ops → 2K ops
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
// ❌ Incorrect: O(n) per lookup
|
|
385
|
+
return orders.map(order => ({
|
|
386
|
+
...order,
|
|
387
|
+
user: users.find(u => u.id === order.userId) // O(n) each time
|
|
388
|
+
}))
|
|
389
|
+
|
|
390
|
+
// ✅ Correct: O(1) per lookup
|
|
391
|
+
const userById = new Map(users.map(u => [u.id, u]))
|
|
392
|
+
return orders.map(order => ({
|
|
393
|
+
...order,
|
|
394
|
+
user: userById.get(order.userId) // O(1)
|
|
395
|
+
}))
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### Cache Property Access in Loops
|
|
399
|
+
|
|
400
|
+
**Impact**: Avoids repeated property lookups
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
// ❌ Incorrect: repeated access
|
|
404
|
+
for (let i = 0; i < items.length; i++) {
|
|
405
|
+
// items.length evaluated each iteration
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ✅ Correct: cached
|
|
409
|
+
const len = items.length
|
|
410
|
+
for (let i = 0; i < len; i++) {
|
|
411
|
+
// Faster
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### Hoist RegExp Creation
|
|
416
|
+
|
|
417
|
+
**Impact**: Avoids creating regex on every call
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
// ❌ Incorrect: creates new RegExp every call
|
|
421
|
+
function validate(email: string) {
|
|
422
|
+
return /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/.test(email)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ✅ Correct: created once
|
|
426
|
+
const EMAIL_REGEX = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/
|
|
427
|
+
|
|
428
|
+
function validate(email: string) {
|
|
429
|
+
return EMAIL_REGEX.test(email)
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### Use toSorted() Instead of sort()
|
|
434
|
+
|
|
435
|
+
**Impact**: Prevents mutation bugs in React state
|
|
436
|
+
|
|
437
|
+
```typescript
|
|
438
|
+
// ❌ Incorrect: mutates array
|
|
439
|
+
setItems(items.sort((a, b) => a - b)) // MUTATION BUG
|
|
440
|
+
|
|
441
|
+
// ✅ Correct: creates new array (Chrome 110+)
|
|
442
|
+
setItems(items.toSorted((a, b) => a - b))
|
|
443
|
+
|
|
444
|
+
// ✅ Alternative: spread + sort
|
|
445
|
+
setItems([...items].sort((a, b) => a - b))
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### Combine Multiple Array Iterations
|
|
449
|
+
|
|
450
|
+
**Impact**: O(3n) → O(n)
|
|
451
|
+
|
|
452
|
+
```typescript
|
|
453
|
+
// ❌ Incorrect: 3 passes
|
|
454
|
+
const filtered = items.filter(x => x.active)
|
|
455
|
+
const mapped = filtered.map(x => x.value)
|
|
456
|
+
const result = mapped.slice(0, 10)
|
|
457
|
+
|
|
458
|
+
// ✅ Correct: 1 pass
|
|
459
|
+
const result = items
|
|
460
|
+
.filter(x => x.active)
|
|
461
|
+
.map(x => x.value)
|
|
462
|
+
.slice(0, 10)
|
|
463
|
+
|
|
464
|
+
// ✅ Even better: reduce for complex transforms
|
|
465
|
+
const result = items.reduce((acc, item) => {
|
|
466
|
+
if (item.active && acc.length < 10) {
|
|
467
|
+
acc.push(item.value)
|
|
468
|
+
}
|
|
469
|
+
return acc
|
|
470
|
+
}, [] as number[])
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
### Use Set/Map for O(1) Lookups
|
|
474
|
+
|
|
475
|
+
**Impact**: O(n) → O(1)
|
|
476
|
+
|
|
477
|
+
```typescript
|
|
478
|
+
// ❌ Incorrect: O(n) lookup
|
|
479
|
+
const isAllowed = (id: string) => allowedIds.includes(id)
|
|
480
|
+
|
|
481
|
+
// ✅ Correct: O(1) lookup
|
|
482
|
+
const allowedSet = new Set(allowedIds)
|
|
483
|
+
const isAllowed = (id: string) => allowedSet.has(id)
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### Early Length Check for Array Comparisons
|
|
487
|
+
|
|
488
|
+
**Impact**: Avoids unnecessary iteration
|
|
489
|
+
|
|
490
|
+
```typescript
|
|
491
|
+
// ❌ Incorrect: iterates even when lengths differ
|
|
492
|
+
function arraysEqual(a: any[], b: any[]) {
|
|
493
|
+
return a.every((item, i) => item === b[i])
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// ✅ Correct: check length first
|
|
497
|
+
function arraysEqual(a: any[], b: any[]) {
|
|
498
|
+
if (a.length !== b.length) return false
|
|
499
|
+
return a.every((item, i) => item === b[i])
|
|
500
|
+
}
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
## Advanced Patterns
|
|
504
|
+
|
|
505
|
+
### Store Event Handlers in Refs
|
|
506
|
+
|
|
507
|
+
**Impact**: Stable references without useCallback
|
|
508
|
+
|
|
509
|
+
```typescript
|
|
510
|
+
// ❌ Incorrect: needs dependencies
|
|
511
|
+
const handleClick = useCallback(() => {
|
|
512
|
+
doSomething(prop1, prop2)
|
|
513
|
+
}, [prop1, prop2]) // Changes often
|
|
514
|
+
|
|
515
|
+
// ✅ Correct: always stable
|
|
516
|
+
const handleClickRef = useRef<() => void>()
|
|
517
|
+
handleClickRef.current = () => {
|
|
518
|
+
doSomething(prop1, prop2)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const handleClick = useCallback(() => {
|
|
522
|
+
handleClickRef.current?.()
|
|
523
|
+
}, []) // Never changes
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### useLatest for Stable Callback Refs
|
|
527
|
+
|
|
528
|
+
**Impact**: Avoids stale closures with stable identity
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
function useLatest<T>(value: T) {
|
|
532
|
+
const ref = useRef(value)
|
|
533
|
+
useEffect(() => {
|
|
534
|
+
ref.current = value
|
|
535
|
+
})
|
|
536
|
+
return ref
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Usage
|
|
540
|
+
function Component({ onEvent }) {
|
|
541
|
+
const onEventRef = useLatest(onEvent)
|
|
542
|
+
|
|
543
|
+
useEffect(() => {
|
|
544
|
+
const handler = () => onEventRef.current()
|
|
545
|
+
window.addEventListener('resize', handler)
|
|
546
|
+
return () => window.removeEventListener('resize', handler)
|
|
547
|
+
}, []) // Empty deps, always uses latest
|
|
548
|
+
}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
## Anti-Patterns to Avoid
|
|
552
|
+
|
|
553
|
+
1. ❌ Non-functional setState (stale closures)
|
|
554
|
+
2. ❌ Missing lazy initialization for expensive initial state
|
|
555
|
+
3. ❌ State reads in render when only used in callbacks
|
|
556
|
+
4. ❌ Creating RegExp/objects in render
|
|
557
|
+
5. ❌ Non-passive scroll/touch event listeners
|
|
558
|
+
6. ❌ Multiple array iterations when one would suffice
|
|
559
|
+
7. ❌ Using `&&` for conditional rendering with numbers
|
|
560
|
+
8. ❌ Direct `.sort()` mutation in React state
|
|
561
|
+
9. ❌ Storing derived values in state
|
|
562
|
+
10. ❌ Not extracting expensive components to memo
|
|
563
|
+
|
|
564
|
+
## React Compiler Note
|
|
565
|
+
|
|
566
|
+
Many of these optimizations become automatic with React Compiler:
|
|
567
|
+
- Auto-memoization of expensive computations
|
|
568
|
+
- Automatic extraction of stable dependencies
|
|
569
|
+
- Smart re-render optimization
|
|
570
|
+
|
|
571
|
+
However, architectural patterns (lazy state init, transitions, content-visibility) still require manual implementation.
|
|
572
|
+
|
|
573
|
+
**Reference**: [Vercel React Best Practices](https://github.com/vercel/react-best-practices)
|