@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.
@@ -0,0 +1,705 @@
1
+ # Next.js RSC & Server Actions Patterns
2
+
3
+ > Critical patterns for React Server Components and Server Actions from Vercel Engineering
4
+
5
+ ## Eliminating Waterfalls
6
+
7
+ ### Defer Await Until Needed
8
+
9
+ **Impact**: HIGH - Avoids blocking unused code paths
10
+
11
+ Move `await` operations into branches where they're actually used:
12
+
13
+ ```typescript
14
+ // ❌ Incorrect: blocks both branches
15
+ async function handleRequest(userId: string, skipProcessing: boolean) {
16
+ const userData = await fetchUserData(userId)
17
+
18
+ if (skipProcessing) {
19
+ return { skipped: true } // Still waited!
20
+ }
21
+
22
+ return processUserData(userData)
23
+ }
24
+
25
+ // ✅ Correct: only blocks when needed
26
+ async function handleRequest(userId: string, skipProcessing: boolean) {
27
+ if (skipProcessing) {
28
+ return { skipped: true } // No wait
29
+ }
30
+
31
+ const userData = await fetchUserData(userId)
32
+ return processUserData(userData)
33
+ }
34
+ ```
35
+
36
+ **Early return optimization**:
37
+
38
+ ```typescript
39
+ // ❌ Incorrect: always fetches permissions
40
+ async function updateResource(resourceId: string, userId: string) {
41
+ const permissions = await fetchPermissions(userId)
42
+ const resource = await getResource(resourceId)
43
+
44
+ if (!resource) return { error: 'Not found' }
45
+ if (!permissions.canEdit) return { error: 'Forbidden' }
46
+
47
+ return await updateResourceData(resource, permissions)
48
+ }
49
+
50
+ // ✅ Correct: check resource first (cheaper)
51
+ async function updateResource(resourceId: string, userId: string) {
52
+ const resource = await getResource(resourceId)
53
+ if (!resource) return { error: 'Not found' }
54
+
55
+ const permissions = await fetchPermissions(userId)
56
+ if (!permissions.canEdit) return { error: 'Forbidden' }
57
+
58
+ return await updateResourceData(resource, permissions)
59
+ }
60
+ ```
61
+
62
+ ### Dependency-Based Parallelization
63
+
64
+ **Impact**: CRITICAL - 2-10× improvement
65
+
66
+ Use `better-all` for partial dependencies:
67
+
68
+ ```typescript
69
+ // ❌ Incorrect: profile waits for config unnecessarily
70
+ const [user, config] = await Promise.all([
71
+ fetchUser(),
72
+ fetchConfig()
73
+ ])
74
+ const profile = await fetchProfile(user.id)
75
+
76
+ // ✅ Correct: config and profile run in parallel
77
+ import { all } from 'better-all'
78
+
79
+ const { user, config, profile } = await all({
80
+ async user() { return fetchUser() },
81
+ async config() { return fetchConfig() },
82
+ async profile() {
83
+ return fetchProfile((await this.$.user).id)
84
+ }
85
+ })
86
+ ```
87
+
88
+ ### Prevent Waterfall Chains in API Routes
89
+
90
+ **Impact**: CRITICAL - 2-10× improvement
91
+
92
+ Start independent operations immediately:
93
+
94
+ ```typescript
95
+ // ❌ Incorrect: sequential
96
+ export async function GET(request: Request) {
97
+ const session = await auth()
98
+ const config = await fetchConfig()
99
+ const data = await fetchData(session.user.id)
100
+ return Response.json({ data, config })
101
+ }
102
+
103
+ // ✅ Correct: parallel where possible
104
+ export async function GET(request: Request) {
105
+ const sessionPromise = auth()
106
+ const configPromise = fetchConfig()
107
+
108
+ const session = await sessionPromise
109
+ const [config, data] = await Promise.all([
110
+ configPromise,
111
+ fetchData(session.user.id)
112
+ ])
113
+
114
+ return Response.json({ data, config })
115
+ }
116
+ ```
117
+
118
+ ### Promise.all() for Independent Operations
119
+
120
+ **Impact**: CRITICAL - Reduces N round trips to 1
121
+
122
+ ```typescript
123
+ // ❌ Incorrect: sequential (3 round trips)
124
+ const user = await fetchUser()
125
+ const posts = await fetchPosts()
126
+ const comments = await fetchComments()
127
+
128
+ // ✅ Correct: parallel (1 round trip)
129
+ const [user, posts, comments] = await Promise.all([
130
+ fetchUser(),
131
+ fetchPosts(),
132
+ fetchComments()
133
+ ])
134
+ ```
135
+
136
+ ### Strategic Suspense Boundaries
137
+
138
+ **Impact**: Faster initial paint
139
+
140
+ Show wrapper UI immediately while data streams in:
141
+
142
+ ```tsx
143
+ // ❌ Incorrect: all or nothing
144
+ export default async function Page() {
145
+ const [header, content, sidebar] = await Promise.all([
146
+ fetchHeader(),
147
+ fetchContent(),
148
+ fetchSidebar()
149
+ ])
150
+ return <Layout header={header} content={content} sidebar={sidebar} />
151
+ }
152
+
153
+ // ✅ Correct: streams in progressively
154
+ async function Header() {
155
+ const data = await fetchHeader()
156
+ return <header>{data}</header>
157
+ }
158
+
159
+ async function Content() {
160
+ const data = await fetchContent()
161
+ return <main>{data}</main>
162
+ }
163
+
164
+ async function Sidebar() {
165
+ const data = await fetchSidebar()
166
+ return <aside>{data}</aside>
167
+ }
168
+
169
+ export default function Page() {
170
+ return (
171
+ <>
172
+ <Suspense fallback={<HeaderSkeleton />}>
173
+ <Header />
174
+ </Suspense>
175
+ <Suspense fallback={<ContentSkeleton />}>
176
+ <Content />
177
+ </Suspense>
178
+ <Suspense fallback={<SidebarSkeleton />}>
179
+ <Sidebar />
180
+ </Suspense>
181
+ </>
182
+ )
183
+ }
184
+ ```
185
+
186
+ **Share promises with `use()` hook**:
187
+
188
+ ```tsx
189
+ const dataPromise = fetchData()
190
+
191
+ function ParentComponent() {
192
+ return (
193
+ <>
194
+ <ChildA dataPromise={dataPromise} />
195
+ <ChildB dataPromise={dataPromise} />
196
+ </>
197
+ )
198
+ }
199
+
200
+ function ChildA({ dataPromise }) {
201
+ const data = use(dataPromise) // Shares same promise
202
+ return <div>{data.title}</div>
203
+ }
204
+
205
+ function ChildB({ dataPromise }) {
206
+ const data = use(dataPromise) // Same promise, no duplicate fetch
207
+ return <div>{data.count}</div>
208
+ }
209
+ ```
210
+
211
+ ## Server-Side Performance
212
+
213
+ ### Cross-Request LRU Caching
214
+
215
+ **Impact**: HIGH - Especially with Vercel Fluid Compute
216
+
217
+ `React.cache()` only works within one request. Use LRU cache for sequential user actions:
218
+
219
+ ```typescript
220
+ import { LRUCache } from 'lru-cache'
221
+
222
+ const userCache = new LRUCache<string, User>({
223
+ max: 1000,
224
+ ttl: 5 * 60 * 1000 // 5 minutes
225
+ })
226
+
227
+ export async function getUser(id: string) {
228
+ const cached = userCache.get(id)
229
+ if (cached) return cached
230
+
231
+ const user = await db.user.findUnique({ where: { id } })
232
+ userCache.set(id, user)
233
+ return user
234
+ }
235
+ ```
236
+
237
+ **Why this matters**: With Vercel Fluid Compute, the same instance handles multiple requests from the same user. LRU cache survives across requests.
238
+
239
+ ### Minimize Serialization at RSC Boundaries
240
+
241
+ **Impact**: HIGH - Reduces data transfer size
242
+
243
+ Only pass fields the client actually uses:
244
+
245
+ ```typescript
246
+ // ❌ Incorrect: serializes all 50 fields
247
+ async function Page() {
248
+ const user = await fetchUser() // 50 fields
249
+ return <Profile user={user} /> // Client uses 1 field
250
+ }
251
+
252
+ // ✅ Correct: serializes only 1 field
253
+ async function Page() {
254
+ const user = await fetchUser()
255
+ return <Profile name={user.name} />
256
+ }
257
+
258
+ // ✅ Alternative: transform before passing
259
+ async function Page() {
260
+ const user = await fetchUser()
261
+ const clientData = {
262
+ name: user.name,
263
+ avatar: user.avatar
264
+ }
265
+ return <Profile user={clientData} />
266
+ }
267
+ ```
268
+
269
+ **Why this matters**: Data is embedded in HTML response AND RSC requests. Large objects increase payload size and parsing time.
270
+
271
+ ### Parallel Data Fetching with Component Composition
272
+
273
+ **Impact**: CRITICAL - Eliminates server-side waterfalls
274
+
275
+ RSCs execute sequentially within a tree. Restructure to parallelize:
276
+
277
+ ```tsx
278
+ // ❌ Incorrect: Sidebar waits for Page's fetch
279
+ export default async function Page() {
280
+ const header = await fetchHeader()
281
+ return (
282
+ <div>
283
+ <div>{header}</div>
284
+ <Sidebar /> {/* Waits for header */}
285
+ </div>
286
+ )
287
+ }
288
+
289
+ // ✅ Correct: both fetch simultaneously
290
+ async function Header() {
291
+ const data = await fetchHeader()
292
+ return <div>{data}</div>
293
+ }
294
+
295
+ export default function Page() {
296
+ return (
297
+ <div>
298
+ <Header />
299
+ <Sidebar /> {/* Parallel! */}
300
+ </div>
301
+ )
302
+ }
303
+ ```
304
+
305
+ **Pro tip**: Move async calls into separate components at the same level to maximize parallelism.
306
+
307
+ ### Per-Request Deduplication with React.cache()
308
+
309
+ **Impact**: HIGH - Prevents duplicate queries in one render
310
+
311
+ **Important**: In Next.js, `fetch` is auto-deduplicated. Use `React.cache()` for:
312
+ - Database queries
313
+ - Heavy computations
314
+ - Auth checks
315
+ - File system operations
316
+
317
+ ```typescript
318
+ import { cache } from 'react'
319
+
320
+ // ✅ Correct: multiple calls = one execution
321
+ export const getUser = cache(async (id: string) => {
322
+ return await db.user.findUnique({ where: { id } })
323
+ })
324
+
325
+ // Usage in multiple components
326
+ async function Profile() {
327
+ const user = await getUser('123') // Executes
328
+ return <div>{user.name}</div>
329
+ }
330
+
331
+ async function Avatar() {
332
+ const user = await getUser('123') // Cached!
333
+ return <img src={user.avatar} />
334
+ }
335
+ ```
336
+
337
+ **Warning**: Avoid inline objects as arguments (always cache miss):
338
+
339
+ ```typescript
340
+ // ❌ Incorrect: cache miss every time (different object reference)
341
+ const getData = cache(async (options: { id: string }) => {
342
+ return await db.query(options.id)
343
+ })
344
+ getData({ id: '123' }) // Miss
345
+ getData({ id: '123' }) // Miss (different object!)
346
+
347
+ // ✅ Correct: use primitives or stable references
348
+ const getData = cache(async (id: string) => {
349
+ return await db.query(id)
350
+ })
351
+ getData('123') // Hit
352
+ getData('123') // Hit
353
+ ```
354
+
355
+ ### Use after() for Non-Blocking Operations
356
+
357
+ **Impact**: HIGH - Faster response times
358
+
359
+ Schedule work after response is sent:
360
+
361
+ ```typescript
362
+ import { after } from 'next/server'
363
+
364
+ export async function POST(request: Request) {
365
+ const data = await request.json()
366
+ await updateDatabase(data)
367
+
368
+ after(async () => {
369
+ // These don't block the response
370
+ await logUserAction(data)
371
+ await sendAnalytics(data)
372
+ await invalidateCache(data.id)
373
+ await sendNotification(data.userId)
374
+ })
375
+
376
+ return new Response(JSON.stringify({ status: 'success' }))
377
+ }
378
+ ```
379
+
380
+ **Perfect for**:
381
+ - Analytics
382
+ - Audit logging
383
+ - Notifications
384
+ - Cache invalidation
385
+ - External webhooks
386
+
387
+ ## Bundle Size Optimization
388
+
389
+ ### Avoid Barrel File Imports
390
+
391
+ **Impact**: CRITICAL - 200-800ms import cost, 15-70% faster builds
392
+
393
+ Popular libraries have 1000s of re-exports. Tree-shaking doesn't help:
394
+
395
+ ```typescript
396
+ // ❌ Incorrect: loads 1,583 modules, ~2.8s in dev
397
+ import { Check, X, Menu } from 'lucide-react'
398
+
399
+ // ✅ Correct: loads only 3 modules
400
+ import Check from 'lucide-react/dist/esm/icons/check'
401
+ import X from 'lucide-react/dist/esm/icons/x'
402
+ import Menu from 'lucide-react/dist/esm/icons/menu'
403
+
404
+ // ✅ Best: Next.js 13.5+ (auto-optimized)
405
+ // next.config.js
406
+ module.exports = {
407
+ experimental: {
408
+ optimizePackageImports: ['lucide-react', '@mui/material', '@mui/icons-material']
409
+ }
410
+ }
411
+ // Then use barrel imports normally
412
+ import { Check, X, Menu } from 'lucide-react' // Optimized!
413
+ ```
414
+
415
+ **Affected libraries**: `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `@headlessui/react`, `@radix-ui/react-*`, `lodash`, `ramda`, `date-fns`
416
+
417
+ ### Conditional Module Loading
418
+
419
+ **Impact**: HIGH - Reduces initial bundle
420
+
421
+ Load large modules only when feature is activated:
422
+
423
+ ```typescript
424
+ // ❌ Incorrect: always bundled
425
+ import { processData } from './heavy-library'
426
+
427
+ export default function Component() {
428
+ const [advanced, setAdvanced] = useState(false)
429
+
430
+ if (!advanced) return <SimpleView />
431
+
432
+ return <AdvancedView data={processData(input)} />
433
+ }
434
+
435
+ // ✅ Correct: loaded only when needed
436
+ export default function Component() {
437
+ const [advanced, setAdvanced] = useState(false)
438
+
439
+ if (!advanced) return <SimpleView />
440
+
441
+ const { processData } = await import('./heavy-library')
442
+ return <AdvancedView data={processData(input)} />
443
+ }
444
+ ```
445
+
446
+ **Client-side SSR prevention**:
447
+
448
+ ```typescript
449
+ // Prevents bundling in server bundle
450
+ if (typeof window !== 'undefined') {
451
+ const { heavyClientLib } = await import('./client-only')
452
+ }
453
+ ```
454
+
455
+ ### Defer Non-Critical Third-Party Libraries
456
+
457
+ **Impact**: HIGH - Analytics/monitoring don't block interaction
458
+
459
+ Load after hydration:
460
+
461
+ ```tsx
462
+ // ❌ Incorrect: blocks hydration
463
+ import Analytics from 'analytics-lib'
464
+
465
+ export default function Layout({ children }) {
466
+ return (
467
+ <>
468
+ <Analytics />
469
+ {children}
470
+ </>
471
+ )
472
+ }
473
+
474
+ // ✅ Correct: loads after hydration
475
+ const Analytics = dynamic(() => import('./Analytics'), {
476
+ ssr: false // Client-only
477
+ })
478
+
479
+ export default function Layout({ children }) {
480
+ return (
481
+ <>
482
+ {children}
483
+ <Analytics />
484
+ </>
485
+ )
486
+ }
487
+ ```
488
+
489
+ ### Dynamic Imports for Heavy Components
490
+
491
+ **Impact**: CRITICAL - Directly affects TTI and LCP
492
+
493
+ Example: Monaco Editor (~300KB) should always be dynamic:
494
+
495
+ ```tsx
496
+ // ❌ Incorrect: adds 300KB to initial bundle
497
+ import Editor from '@monaco-editor/react'
498
+
499
+ export default function Page() {
500
+ return <Editor />
501
+ }
502
+
503
+ // ✅ Correct: loaded on demand
504
+ const Editor = dynamic(() => import('@monaco-editor/react'), {
505
+ loading: () => <div>Loading editor...</div>,
506
+ ssr: false
507
+ })
508
+
509
+ export default function Page() {
510
+ return <Editor />
511
+ }
512
+ ```
513
+
514
+ ### Preload Based on User Intent
515
+
516
+ **Impact**: MEDIUM-HIGH - Feels instant
517
+
518
+ Preload before click using hover/focus:
519
+
520
+ ```tsx
521
+ import { useState } from 'react'
522
+
523
+ function NavigationLink({ href, children }) {
524
+ const [isPreloading, setIsPreloading] = useState(false)
525
+
526
+ return (
527
+ <Link
528
+ href={href}
529
+ onMouseEnter={() => {
530
+ if (!isPreloading) {
531
+ setIsPreloading(true)
532
+ // Preload route
533
+ router.prefetch(href)
534
+ }
535
+ }}
536
+ onFocus={() => {
537
+ if (!isPreloading) {
538
+ setIsPreloading(true)
539
+ router.prefetch(href)
540
+ }
541
+ }}
542
+ >
543
+ {children}
544
+ </Link>
545
+ )
546
+ }
547
+ ```
548
+
549
+ ## Data Fetching Strategies
550
+
551
+ ### fetch is Auto-Deduplicated
552
+
553
+ **Built-in**: Same URL + options = one request per render
554
+
555
+ ```tsx
556
+ // ✅ Automatic deduplication
557
+ async function Header() {
558
+ const data = await fetch('/api/data') // Request 1
559
+ return <div>{data.title}</div>
560
+ }
561
+
562
+ async function Content() {
563
+ const data = await fetch('/api/data') // Deduplicated!
564
+ return <div>{data.body}</div>
565
+ }
566
+ ```
567
+
568
+ ### React.cache() for Non-Fetch
569
+
570
+ Use for DB queries, file ops, computations:
571
+
572
+ ```typescript
573
+ import { cache } from 'react'
574
+
575
+ export const getSettings = cache(async () => {
576
+ // Heavy computation or DB query
577
+ return await db.settings.findFirst()
578
+ })
579
+ ```
580
+
581
+ ### Client-Side: Use SWR for Deduplication
582
+
583
+ ```typescript
584
+ import useSWR from 'swr'
585
+
586
+ // Multiple instances share one request
587
+ function Component() {
588
+ const { data } = useSWR('/api/user', fetcher)
589
+ return <div>{data?.name}</div>
590
+ }
591
+
592
+ // Use useImmutableSWR for static data
593
+ import { useImmutableSWR } from 'swr/immutable'
594
+
595
+ function StaticComponent() {
596
+ const { data } = useImmutableSWR('/api/config', fetcher)
597
+ return <div>{data?.appName}</div>
598
+ }
599
+ ```
600
+
601
+ ## Server Actions Best Practices
602
+
603
+ ### Prevent Waterfalls
604
+
605
+ ```typescript
606
+ 'use server'
607
+
608
+ // ❌ Incorrect: sequential
609
+ export async function updateProfile(data: FormData) {
610
+ const user = await getUser()
611
+ const validated = await validateData(data)
612
+ const result = await saveToDb(validated)
613
+ return result
614
+ }
615
+
616
+ // ✅ Correct: parallel where possible
617
+ export async function updateProfile(data: FormData) {
618
+ const userPromise = getUser()
619
+ const validatedPromise = validateData(data)
620
+
621
+ const [user, validated] = await Promise.all([
622
+ userPromise,
623
+ validatedPromise
624
+ ])
625
+
626
+ const result = await saveToDb(validated)
627
+ return result
628
+ }
629
+ ```
630
+
631
+ ### Use after() for Non-Blocking Work
632
+
633
+ ```typescript
634
+ 'use server'
635
+
636
+ import { after } from 'next/server'
637
+
638
+ export async function createPost(data: FormData) {
639
+ const post = await db.post.create({ data })
640
+
641
+ after(async () => {
642
+ await revalidatePath('/blog')
643
+ await sendNotification(post.authorId)
644
+ await trackEvent('post_created', { postId: post.id })
645
+ })
646
+
647
+ return { success: true, postId: post.id }
648
+ }
649
+ ```
650
+
651
+ ### React.cache() for Shared Logic
652
+
653
+ ```typescript
654
+ import { cache } from 'react'
655
+
656
+ export const getCurrentUser = cache(async () => {
657
+ const session = await auth()
658
+ if (!session) return null
659
+ return await db.user.findUnique({ where: { id: session.userId } })
660
+ })
661
+
662
+ // Multiple server actions can call this
663
+ export async function updateProfile(data: FormData) {
664
+ const user = await getCurrentUser() // Cached
665
+ // ...
666
+ }
667
+
668
+ export async function deleteAccount() {
669
+ const user = await getCurrentUser() // Same cache
670
+ // ...
671
+ }
672
+ ```
673
+
674
+ ## Anti-Patterns to Avoid
675
+
676
+ ### Critical Anti-Patterns
677
+ 1. ❌ Sequential awaits for independent operations
678
+ 2. ❌ Barrel file imports (200-800ms overhead)
679
+ 3. ❌ Awaiting in parent RSC before rendering children
680
+ 4. ❌ Passing entire objects across RSC boundary
681
+
682
+ ### High-Impact Anti-Patterns
683
+ 5. ❌ No deduplication for repeated calls (use React.cache/SWR)
684
+ 6. ❌ Blocking responses with logging/analytics (use after())
685
+ 7. ❌ Inline objects as React.cache() arguments
686
+ 8. ❌ Not using optimizePackageImports for icon libraries
687
+
688
+ ### Subtle Bugs
689
+ 9. ❌ Forgetting `ssr: false` for client-only libraries
690
+ 10. ❌ Not hoisting async calls to enable parallelization
691
+ 11. ❌ Using barrel imports in dev (slow HMR)
692
+ 12. ❌ LRU cache without TTL (memory leak)
693
+
694
+ ## Key Metrics
695
+
696
+ | Optimization | Improvement |
697
+ |---|---|
698
+ | Eliminate waterfalls | 2-10× faster |
699
+ | Avoid barrel imports | 15-70% faster builds, 28% faster HMR |
700
+ | Bundle size reduction | Direct TTI/LCP impact |
701
+ | LRU caching | Eliminates redundant DB queries |
702
+ | Parallel RSC composition | Eliminates server waterfalls |
703
+ | after() for non-blocking | 50-200ms faster responses |
704
+
705
+ **Reference**: [Vercel React Best Practices](https://github.com/vercel/react-best-practices)