RuleStack

Configs

Stacks

Compare

Diff

RuleStack

Configs

Stacks

Compare

Diff

Read API

RuleStack

Configs

Stacks

Compare

Diff

Read API

Configs/zebbern/claude-code-guide/diff

Two files, one repository

zebbern/claude-code-guide ships 1 format across 2 indexed files. The question worth asking is whether the second one says anything the first does not.

A · skills/react-best-practices/AGENTS.md · 9811 wordsB · skills/composition-patterns/AGENTS.md · 2495 words
What each file covers, counted
DimensionSharedOnly in AOnly in BOverlap
Sections258143%
Commands0200%
Section tags48033%

What each file covers

Sections

2 shared · 58 only in A · 14 only in B
  • − React Best Practices
  • − 1. Eliminating Waterfalls
  • − 1.1 Defer Await Until Needed
  • − 1.2 Dependency-Based Parallelization
  • − 1.3 Prevent Waterfall Chains in API Routes
  • − 1.4 Promise.all() for Independent Operations
  • − 1.5 Strategic Suspense Boundaries
  • − 2. Bundle Size Optimization
  • − 2.1 Avoid Barrel File Imports
  • − 2.2 Conditional Module Loading
  • − 2.3 Defer Non-Critical Third-Party Libraries
  • − 2.4 Dynamic Imports for Heavy Components
  • − 2.5 Preload Based on User Intent
  • − 3. Server-Side Performance
  • − 3.1 Authenticate Server Actions Like API Routes
  • − 3.2 Avoid Duplicate Serialization in RSC Props
  • − 3.3 Cross-Request LRU Caching
  • − 3.4 Minimize Serialization at RSC Boundaries
  • − 3.5 Parallel Data Fetching with Component Composition
  • − 3.6 Per-Request Deduplication with React.cache()
  • − 3.7 Use after() for Non-Blocking Operations
  • − 4. Client-Side Data Fetching
  • − 4.1 Deduplicate Global Event Listeners
  • − 4.2 Use Passive Event Listeners for Scrolling Performance
  • − 4.3 Use SWR for Automatic Deduplication
  • − 4.4 Version and Minimize localStorage Data
  • − 5. Re-render Optimization
  • − 5.1 Calculate Derived State During Rendering
  • − 5.2 Defer State Reads to Usage Point
  • − 5.3 Do not wrap a simple expression with a primitive result type in useMemo
  • − 5.4 Extract Default Non-primitive Parameter Value from Memoized Component to Constant
  • − 5.5 Extract to Memoized Components
  • − 5.6 Narrow Effect Dependencies
  • − 5.7 Put Interaction Logic in Event Handlers
  • − 5.8 Subscribe to Derived State
  • − 5.9 Use Functional setState Updates
  • − 5.10 Use Lazy State Initialization
  • − 5.11 Use Transitions for Non-Urgent Updates
  • − 5.12 Use useRef for Transient Values
  • − 6. Rendering Performance
  • − 6.1 Animate SVG Wrapper Instead of SVG Element
  • − 6.2 CSS content-visibility for Long Lists
  • − 6.3 Hoist Static JSX Elements
  • − 6.4 Optimize SVG Precision
  • − 6.5 Prevent Hydration Mismatch Without Flickering
  • − 6.6 Suppress Expected Hydration Mismatches
  • − 6.7 Use Activity Component for Show/Hide
  • − 6.8 Use Explicit Conditional Rendering
  • − 6.9 Use useTransition Over Manual Loading States
  • − 7. JavaScript Performance
  • − 7.1 Avoid Layout Thrashing
  • − 7.2 Build Index Maps for Repeated Lookups
  • − 7.3 Cache Property Access in Loops
  • − 7.4 Cache Repeated Function Calls
  • − 7.5 Cache Storage API Calls
  • − 7.6 Combine Multiple Array Iterations
  • − 7.7 Early Length Check for Array Comparisons
  • − 7.8 Early Return from Functions
  • + React Composition Patterns
  • + 1. Component Architecture
  • + 1.1 Avoid Boolean Prop Proliferation
  • + 1.2 Use Compound Components
  • + 2. State Management
  • + 2.1 Decouple State Management from UI
  • + 2.2 Define Generic Context Interfaces for Dependency Injection
  • + 2.3 Lift State into Provider Components
  • + 3. Implementation Patterns
  • + 3.1 Create Explicit Component Variants
  • + 3.2 Prefer Composing Children Over Render Props
  • + 4. React 19 APIs
  • + 4.1 React 19 API Changes
  • + References
  •   Abstract
  •   Table of Contents

Commands

0 shared · 2 only in A · 0 only in B
  • − node.style.transform = `translateX(${e.clientX}px)`
  • − npx svgo --precision=1 --multipass icon.svg

Section tags

4 shared · 8 only in A · 0 only in B
  • − build
  • − lint-format
  • − architecture
  • − types
  • − security
  • − dependencies
  • − performance
  • − deployment
  •   code-style
  •   api
  •   ui
  •   do-not

Line diff

+917 added−2883 removed1 unchanged0.0% identical
zebbern/claude-code-guide · skills/react-best-practices/AGENTS.md
@@ −1 @@
1# React Best Practices
2 
3**Version 1.0.0**
4Vercel Engineering
5January 2026
6 
7> **Note:**
8> This document is mainly for agents and LLMs to follow when maintaining,
9> generating, or refactoring React and Next.js codebases. Humans
10> may also find it useful, but guidance here is optimized for automation
11> and consistency by AI-assisted workflows.
12 
13---
14 
15## Abstract
16 
17Comprehensive performance optimization guide for React and Next.js applications, designed for AI agents and LLMs. Contains 40+ rules across 8 categories, prioritized by impact from critical (eliminating waterfalls, reducing bundle size) to incremental (advanced patterns). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation.
18 
19---
20 
21## Table of Contents
22 
231. [Eliminating Waterfalls](#1-eliminating-waterfalls) — **CRITICAL**
24 - 1.1 [Defer Await Until Needed](#11-defer-await-until-needed)
25 - 1.2 [Dependency-Based Parallelization](#12-dependency-based-parallelization)
26 - 1.3 [Prevent Waterfall Chains in API Routes](#13-prevent-waterfall-chains-in-api-routes)
27 - 1.4 [Promise.all() for Independent Operations](#14-promiseall-for-independent-operations)
28 - 1.5 [Strategic Suspense Boundaries](#15-strategic-suspense-boundaries)
292. [Bundle Size Optimization](#2-bundle-size-optimization) — **CRITICAL**
30 - 2.1 [Avoid Barrel File Imports](#21-avoid-barrel-file-imports)
31 - 2.2 [Conditional Module Loading](#22-conditional-module-loading)
32 - 2.3 [Defer Non-Critical Third-Party Libraries](#23-defer-non-critical-third-party-libraries)
33 - 2.4 [Dynamic Imports for Heavy Components](#24-dynamic-imports-for-heavy-components)
34 - 2.5 [Preload Based on User Intent](#25-preload-based-on-user-intent)
353. [Server-Side Performance](#3-server-side-performance) — **HIGH**
36 - 3.1 [Authenticate Server Actions Like API Routes](#31-authenticate-server-actions-like-api-routes)
37 - 3.2 [Avoid Duplicate Serialization in RSC Props](#32-avoid-duplicate-serialization-in-rsc-props)
38 - 3.3 [Cross-Request LRU Caching](#33-cross-request-lru-caching)
39 - 3.4 [Minimize Serialization at RSC Boundaries](#34-minimize-serialization-at-rsc-boundaries)
40 - 3.5 [Parallel Data Fetching with Component Composition](#35-parallel-data-fetching-with-component-composition)
41 - 3.6 [Per-Request Deduplication with React.cache()](#36-per-request-deduplication-with-reactcache)
42 - 3.7 [Use after() for Non-Blocking Operations](#37-use-after-for-non-blocking-operations)
434. [Client-Side Data Fetching](#4-client-side-data-fetching) — **MEDIUM-HIGH**
44 - 4.1 [Deduplicate Global Event Listeners](#41-deduplicate-global-event-listeners)
45 - 4.2 [Use Passive Event Listeners for Scrolling Performance](#42-use-passive-event-listeners-for-scrolling-performance)
46 - 4.3 [Use SWR for Automatic Deduplication](#43-use-swr-for-automatic-deduplication)
47 - 4.4 [Version and Minimize localStorage Data](#44-version-and-minimize-localstorage-data)
485. [Re-render Optimization](#5-re-render-optimization) — **MEDIUM**
49 - 5.1 [Calculate Derived State During Rendering](#51-calculate-derived-state-during-rendering)
50 - 5.2 [Defer State Reads to Usage Point](#52-defer-state-reads-to-usage-point)
51 - 5.3 [Do not wrap a simple expression with a primitive result type in useMemo](#53-do-not-wrap-a-simple-expression-with-a-primitive-result-type-in-usememo)
52 - 5.4 [Extract Default Non-primitive Parameter Value from Memoized Component to Constant](#54-extract-default-non-primitive-parameter-value-from-memoized-component-to-constant)
53 - 5.5 [Extract to Memoized Components](#55-extract-to-memoized-components)
54 - 5.6 [Narrow Effect Dependencies](#56-narrow-effect-dependencies)
55 - 5.7 [Put Interaction Logic in Event Handlers](#57-put-interaction-logic-in-event-handlers)
56 - 5.8 [Subscribe to Derived State](#58-subscribe-to-derived-state)
57 - 5.9 [Use Functional setState Updates](#59-use-functional-setstate-updates)
58 - 5.10 [Use Lazy State Initialization](#510-use-lazy-state-initialization)
59 - 5.11 [Use Transitions for Non-Urgent Updates](#511-use-transitions-for-non-urgent-updates)
60 - 5.12 [Use useRef for Transient Values](#512-use-useref-for-transient-values)
616. [Rendering Performance](#6-rendering-performance) — **MEDIUM**
62 - 6.1 [Animate SVG Wrapper Instead of SVG Element](#61-animate-svg-wrapper-instead-of-svg-element)
63 - 6.2 [CSS content-visibility for Long Lists](#62-css-content-visibility-for-long-lists)
64 - 6.3 [Hoist Static JSX Elements](#63-hoist-static-jsx-elements)
65 - 6.4 [Optimize SVG Precision](#64-optimize-svg-precision)
66 - 6.5 [Prevent Hydration Mismatch Without Flickering](#65-prevent-hydration-mismatch-without-flickering)
67 - 6.6 [Suppress Expected Hydration Mismatches](#66-suppress-expected-hydration-mismatches)
68 - 6.7 [Use Activity Component for Show/Hide](#67-use-activity-component-for-showhide)
69 - 6.8 [Use Explicit Conditional Rendering](#68-use-explicit-conditional-rendering)
70 - 6.9 [Use useTransition Over Manual Loading States](#69-use-usetransition-over-manual-loading-states)
717. [JavaScript Performance](#7-javascript-performance) — **LOW-MEDIUM**
72 - 7.1 [Avoid Layout Thrashing](#71-avoid-layout-thrashing)
73 - 7.2 [Build Index Maps for Repeated Lookups](#72-build-index-maps-for-repeated-lookups)
74 - 7.3 [Cache Property Access in Loops](#73-cache-property-access-in-loops)
75 - 7.4 [Cache Repeated Function Calls](#74-cache-repeated-function-calls)
76 - 7.5 [Cache Storage API Calls](#75-cache-storage-api-calls)
77 - 7.6 [Combine Multiple Array Iterations](#76-combine-multiple-array-iterations)
78 - 7.7 [Early Length Check for Array Comparisons](#77-early-length-check-for-array-comparisons)
79 - 7.8 [Early Return from Functions](#78-early-return-from-functions)
80 - 7.9 [Hoist RegExp Creation](#79-hoist-regexp-creation)
81 - 7.10 [Use Loop for Min/Max Instead of Sort](#710-use-loop-for-minmax-instead-of-sort)
82 - 7.11 [Use Set/Map for O(1) Lookups](#711-use-setmap-for-o1-lookups)
83 - 7.12 [Use toSorted() Instead of sort() for Immutability](#712-use-tosorted-instead-of-sort-for-immutability)
848. [Advanced Patterns](#8-advanced-patterns) — **LOW**
85 - 8.1 [Initialize App Once, Not Per Mount](#81-initialize-app-once-not-per-mount)
86 - 8.2 [Store Event Handlers in Refs](#82-store-event-handlers-in-refs)
87 - 8.3 [useEffectEvent for Stable Callback Refs](#83-useeffectevent-for-stable-callback-refs)
88 
89---
90 
91## 1. Eliminating Waterfalls
92 
93**Impact: CRITICAL**
94 
95Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains.
96 
97### 1.1 Defer Await Until Needed
98 
99**Impact: HIGH (avoids blocking unused code paths)**
100 
101Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them.
102 
103**Incorrect: blocks both branches**
104 
105```typescript
106async function handleRequest(userId: string, skipProcessing: boolean) {
107 const userData = await fetchUserData(userId)
108 
109 if (skipProcessing) {
110 // Returns immediately but still waited for userData
111 return { skipped: true }
112 }
113 
114 // Only this branch uses userData
115 return processUserData(userData)
116}
117```
118 
119**Correct: only blocks when needed**
120 
121```typescript
122async function handleRequest(userId: string, skipProcessing: boolean) {
123 if (skipProcessing) {
124 // Returns immediately without waiting
125 return { skipped: true }
126 }
127 
128 // Fetch only when needed
129 const userData = await fetchUserData(userId)
130 return processUserData(userData)
131}
132```
133 
134**Another example: early return optimization**
135 
136```typescript
137// Incorrect: always fetches permissions
138async function updateResource(resourceId: string, userId: string) {
139 const permissions = await fetchPermissions(userId)
140 const resource = await getResource(resourceId)
141 
142 if (!resource) {
143 return { error: "Not found" }
144 }
145 
146 if (!permissions.canEdit) {
147 return { error: "Forbidden" }
148 }
149 
150 return await updateResourceData(resource, permissions)
151}
152 
153// Correct: fetches only when needed
154async function updateResource(resourceId: string, userId: string) {
155 const resource = await getResource(resourceId)
156 
157 if (!resource) {
158 return { error: "Not found" }
159 }
160 
161 const permissions = await fetchPermissions(userId)
162 
163 if (!permissions.canEdit) {
164 return { error: "Forbidden" }
165 }
166 
167 return await updateResourceData(resource, permissions)
168}
169```
170 
171This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive.
172 
173### 1.2 Dependency-Based Parallelization
174 
175**Impact: CRITICAL (2-10× improvement)**
176 
177For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment.
178 
179**Incorrect: profile waits for config unnecessarily**
180 
181```typescript
182const [user, config] = await Promise.all([fetchUser(), fetchConfig()])
183const profile = await fetchProfile(user.id)
184```
185 
186**Correct: config and profile run in parallel**
187 
188```typescript
189import { all } from "better-all"
190 
191const { user, config, profile } = await all({
192 async user() {
193 return fetchUser()
194 },
195 async config() {
196 return fetchConfig()
197 },
198 async profile() {
199 return fetchProfile((await this.$.user).id)
200 },
201})
202```
203 
204**Alternative without extra dependencies:**
205 
206```typescript
207const userPromise = fetchUser()
208const profilePromise = userPromise.then((user) => fetchProfile(user.id))
209 
210const [user, config, profile] = await Promise.all([userPromise, fetchConfig(), profilePromise])
211```
212 
213We can also create all the promises first, and do `Promise.all()` at the end.
214 
215Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
216 
217### 1.3 Prevent Waterfall Chains in API Routes
218 
219**Impact: CRITICAL (2-10× improvement)**
220 
221In API routes and Server Actions, start independent operations immediately, even if you don't await them yet.
222 
223**Incorrect: config waits for auth, data waits for both**
224 
225```typescript
226export async function GET(request: Request) {
227 const session = await auth()
228 const config = await fetchConfig()
229 const data = await fetchData(session.user.id)
230 return Response.json({ data, config })
231}
232```
233 
234**Correct: auth and config start immediately**
235 
236```typescript
237export async function GET(request: Request) {
238 const sessionPromise = auth()
239 const configPromise = fetchConfig()
240 const session = await sessionPromise
241 const [config, data] = await Promise.all([configPromise, fetchData(session.user.id)])
242 return Response.json({ data, config })
243}
244```
245 
246For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization).
247 
248### 1.4 Promise.all() for Independent Operations
249 
250**Impact: CRITICAL (2-10× improvement)**
251 
252When async operations have no interdependencies, execute them concurrently using `Promise.all()`.
253 
254**Incorrect: sequential execution, 3 round trips**
255 
256```typescript
257const user = await fetchUser()
258const posts = await fetchPosts()
259const comments = await fetchComments()
260```
261 
262**Correct: parallel execution, 1 round trip**
263 
264```typescript
265const [user, posts, comments] = await Promise.all([fetchUser(), fetchPosts(), fetchComments()])
266```
267 
268### 1.5 Strategic Suspense Boundaries
269 
270**Impact: HIGH (faster initial paint)**
271 
272Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads.
273 
274**Incorrect: wrapper blocked by data fetching**
275 
276```tsx
277async function Page() {
278 const data = await fetchData() // Blocks entire page
279 
280 return (
281 <div>
282 <div>Sidebar</div>
283 <div>Header</div>
284 <div>
285 <DataDisplay data={data} />
286 </div>
287 <div>Footer</div>
288 </div>
289 )
290}
291```
292 
293The entire layout waits for data even though only the middle section needs it.
294 
295**Correct: wrapper shows immediately, data streams in**
296 
297```tsx
298function Page() {
299 return (
300 <div>
301 <div>Sidebar</div>
302 <div>Header</div>
303 <div>
304 <Suspense fallback={<Skeleton />}>
305 <DataDisplay />
306 </Suspense>
307 </div>
308 <div>Footer</div>
309 </div>
310 )
311}
312 
313async function DataDisplay() {
314 const data = await fetchData() // Only blocks this component
315 return <div>{data.content}</div>
316}
317```
318 
319Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data.
320 
321**Alternative: share promise across components**
322 
323```tsx
324function Page() {
325 // Start fetch immediately, but don't await
326 const dataPromise = fetchData()
327 
328 return (
329 <div>
330 <div>Sidebar</div>
331 <div>Header</div>
332 <Suspense fallback={<Skeleton />}>
333 <DataDisplay dataPromise={dataPromise} />
334 <DataSummary dataPromise={dataPromise} />
335 </Suspense>
336 <div>Footer</div>
337 </div>
338 )
339}
340 
341function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {
342 const data = use(dataPromise) // Unwraps the promise
343 return <div>{data.content}</div>
344}
345 
346function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {
347 const data = use(dataPromise) // Reuses the same promise
348 return <div>{data.summary}</div>
349}
350```
351 
352Both components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together.
353 
354**When NOT to use this pattern:**
355 
356- Critical data needed for layout decisions (affects positioning)
357 
358- SEO-critical content above the fold
359 
360- Small, fast queries where suspense overhead isn't worth it
361 
362- When you want to avoid layout shift (loading → content jump)
363 
364**Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities.
365 
366---
367 
368## 2. Bundle Size Optimization
369 
370**Impact: CRITICAL**
371 
372Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint.
373 
374### 2.1 Avoid Barrel File Imports
375 
376**Impact: CRITICAL (200-800ms import cost, slow builds)**
377 
378Import directly from source files instead of barrel files to avoid loading thousands of unused modules. **Barrel files** are entry points that re-export multiple modules (e.g., `index.js` that does `export * from './module'`).
379 
380Popular icon and component libraries can have **up to 10,000 re-exports** in their entry file. For many React packages, **it takes 200-800ms just to import them**, affecting both development speed and production cold starts.
381 
382**Why tree-shaking doesn't help:** When a library is marked as external (not bundled), the bundler can't optimize it. If you bundle it to enable tree-shaking, builds become substantially slower analyzing the entire module graph.
383 
384**Incorrect: imports entire library**
385 
386```tsx
387import { Check, X, Menu } from "lucide-react"
388// Loads 1,583 modules, takes ~2.8s extra in dev
389// Runtime cost: 200-800ms on every cold start
390 
391import { Button, TextField } from "@mui/material"
392// Loads 2,225 modules, takes ~4.2s extra in dev
393```
394 
395**Correct: imports only what you need**
396 
397```tsx
398import Check from "lucide-react/dist/esm/icons/check"
399import X from "lucide-react/dist/esm/icons/x"
400import Menu from "lucide-react/dist/esm/icons/menu"
401// Loads only 3 modules (~2KB vs ~1MB)
402 
403import Button from "@mui/material/Button"
404import TextField from "@mui/material/TextField"
405// Loads only what you use
406```
407 
408**Alternative: Next.js 13.5+**
409 
410```js
411// next.config.js - use optimizePackageImports
412module.exports = {
413 experimental: {
414 optimizePackageImports: ["lucide-react", "@mui/material"],
415 },
416}
417 
418// Then you can keep the ergonomic barrel imports:
419import { Check, X, Menu } from "lucide-react"
420// Automatically transformed to direct imports at build time
421```
422 
423Direct imports provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR.
424 
425Libraries commonly affected: `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `@headlessui/react`, `@radix-ui/react-*`, `lodash`, `ramda`, `date-fns`, `rxjs`, `react-use`.
426 
427Reference: [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)
428 
429### 2.2 Conditional Module Loading
430 
431**Impact: HIGH (loads large data only when needed)**
432 
433Load large data or modules only when a feature is activated.
434 
435**Example: lazy-load animation frames**
436 
437```tsx
438function AnimationPlayer({
439 enabled,
440 setEnabled,
441}: {
442 enabled: boolean
443 setEnabled: React.Dispatch<React.SetStateAction<boolean>>
444}) {
445 const [frames, setFrames] = useState<Frame[] | null>(null)
446 
447 useEffect(() => {
448 if (enabled && !frames && typeof window !== "undefined") {
449 import("./animation-frames.js")
450 .then((mod) => setFrames(mod.frames))
451 .catch(() => setEnabled(false))
452 }
453 }, [enabled, frames, setEnabled])
454 
455 if (!frames) return <Skeleton />
456 return <Canvas frames={frames} />
457}
458```
459 
460The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed.
461 
462### 2.3 Defer Non-Critical Third-Party Libraries
463 
464**Impact: MEDIUM (loads after hydration)**
465 
466Analytics, logging, and error tracking don't block user interaction. Load them after hydration.
467 
468**Incorrect: blocks initial bundle**
469 
470```tsx
471import { Analytics } from "@vercel/analytics/react"
472 
473export default function RootLayout({ children }) {
474 return (
475 <html>
476 <body>
477 {children}
478 <Analytics />
479 </body>
480 </html>
481 )
482}
483```
484 
485**Correct: loads after hydration**
486 
487```tsx
488import dynamic from "next/dynamic"
489 
490const Analytics = dynamic(() => import("@vercel/analytics/react").then((m) => m.Analytics), {
491 ssr: false,
492})
493 
494export default function RootLayout({ children }) {
495 return (
496 <html>
497 <body>
498 {children}
499 <Analytics />
500 </body>
501 </html>
502 )
503}
504```
505 
506### 2.4 Dynamic Imports for Heavy Components
507 
508**Impact: CRITICAL (directly affects TTI and LCP)**
509 
510Use `next/dynamic` to lazy-load large components not needed on initial render.
511 
512**Incorrect: Monaco bundles with main chunk ~300KB**
513 
514```tsx
515import { MonacoEditor } from "./monaco-editor"
516 
517function CodePanel({ code }: { code: string }) {
518 return <MonacoEditor value={code} />
519}
520```
521 
522**Correct: Monaco loads on demand**
523 
524```tsx
525import dynamic from "next/dynamic"
526 
527const MonacoEditor = dynamic(() => import("./monaco-editor").then((m) => m.MonacoEditor), {
528 ssr: false,
529})
530 
531function CodePanel({ code }: { code: string }) {
532 return <MonacoEditor value={code} />
533}
534```
535 
536### 2.5 Preload Based on User Intent
537 
538**Impact: MEDIUM (reduces perceived latency)**
539 
540Preload heavy bundles before they're needed to reduce perceived latency.
541 
542**Example: preload on hover/focus**
543 
544```tsx
545function EditorButton({ onClick }: { onClick: () => void }) {
546 const preload = () => {
547 if (typeof window !== "undefined") {
548 void import("./monaco-editor")
549 }
550 }
551 
552 return (
553 <button onMouseEnter={preload} onFocus={preload} onClick={onClick}>
554 Open Editor
555 </button>
556 )
557}
558```
559 
560**Example: preload when feature flag is enabled**
561 
562```tsx
563function FlagsProvider({ children, flags }: Props) {
564 useEffect(() => {
565 if (flags.editorEnabled && typeof window !== "undefined") {
566 void import("./monaco-editor").then((mod) => mod.init())
567 }
568 }, [flags.editorEnabled])
569 
570 return <FlagsContext.Provider value={flags}>{children}</FlagsContext.Provider>
571}
572```
573 
574The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed.
575 
576---
577 
578## 3. Server-Side Performance
579 
580**Impact: HIGH**
581 
582Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times.
583 
584### 3.1 Authenticate Server Actions Like API Routes
585 
586**Impact: CRITICAL (prevents unauthorized access to server mutations)**
587 
588Server Actions (functions with `"use server"`) are exposed as public endpoints, just like API routes. Always verify authentication and authorization **inside** each Server Action—do not rely solely on middleware, layout guards, or page-level checks, as Server Actions can be invoked directly.
589 
590Next.js documentation explicitly states: "Treat Server Actions with the same security considerations as public-facing API endpoints, and verify if the user is allowed to perform a mutation."
591 
592**Incorrect: no authentication check**
593 
594```typescript
595"use server"
596 
597export async function deleteUser(userId: string) {
598 // Anyone can call this! No auth check
599 await db.user.delete({ where: { id: userId } })
600 return { success: true }
601}
602```
603 
604**Correct: authentication inside the action**
605 
606```typescript
607"use server"
608 
609import { verifySession } from "@/lib/auth"
610import { unauthorized } from "@/lib/errors"
611 
612export async function deleteUser(userId: string) {
613 // Always check auth inside the action
614 const session = await verifySession()
615 
616 if (!session) {
617 throw unauthorized("Must be logged in")
618 }
619 
620 // Check authorization too
621 if (session.user.role !== "admin" && session.user.id !== userId) {
622 throw unauthorized("Cannot delete other users")
623 }
624 
625 await db.user.delete({ where: { id: userId } })
626 return { success: true }
627}
628```
629 
630**With input validation:**
631 
632```typescript
633"use server"
634 
635import { verifySession } from "@/lib/auth"
636import { z } from "zod"
637 
638const updateProfileSchema = z.object({
639 userId: z.string().uuid(),
640 name: z.string().min(1).max(100),
641 email: z.string().email(),
642})
643 
644export async function updateProfile(data: unknown) {
645 // Validate input first
646 const validated = updateProfileSchema.parse(data)
647 
648 // Then authenticate
649 const session = await verifySession()
650 if (!session) {
651 throw new Error("Unauthorized")
652 }
653 
654 // Then authorize
655 if (session.user.id !== validated.userId) {
656 throw new Error("Can only update own profile")
657 }
658 
659 // Finally perform the mutation
660 await db.user.update({
661 where: { id: validated.userId },
662 data: {
663 name: validated.name,
664 email: validated.email,
665 },
666 })
667 
668 return { success: true }
669}
670```
671 
672Reference: [https://nextjs.org/docs/app/guides/authentication](https://nextjs.org/docs/app/guides/authentication)
673 
674### 3.2 Avoid Duplicate Serialization in RSC Props
675 
676**Impact: LOW (reduces network payload by avoiding duplicate serialization)**
677 
678RSC→client serialization deduplicates by object reference, not value. Same reference = serialized once; new reference = serialized again. Do transformations (`.toSorted()`, `.filter()`, `.map()`) in client, not server.
679 
680**Incorrect: duplicates array**
681 
682```tsx
683// RSC: sends 6 strings (2 arrays × 3 items)
684<ClientList usernames={usernames} usernamesOrdered={usernames.toSorted()} />
685```
686 
687**Correct: sends 3 strings**
688 
689```tsx
690// RSC: send once
691;<ClientList usernames={usernames} />
692 
693// Client: transform there
694;("use client")
695const sorted = useMemo(() => [...usernames].sort(), [usernames])
696```
697 
698**Nested deduplication behavior:**
699 
700```tsx
701// string[] - duplicates everything
702usernames={['a','b']} sorted={usernames.toSorted()} // sends 4 strings
703 
704// object[] - duplicates array structure only
705users={[{id:1},{id:2}]} sorted={users.toSorted()} // sends 2 arrays + 2 unique objects (not 4)
706```
707 
708Deduplication works recursively. Impact varies by data type:
709 
710- `string[]`, `number[]`, `boolean[]`: **HIGH impact** - array + all primitives fully duplicated
711 
712- `object[]`: **LOW impact** - array duplicated, but nested objects deduplicated by reference
713 
714**Operations breaking deduplication: create new references**
715 
716- Arrays: `.toSorted()`, `.filter()`, `.map()`, `.slice()`, `[...arr]`
717 
718- Objects: `{...obj}`, `Object.assign()`, `structuredClone()`, `JSON.parse(JSON.stringify())`
719 
720**More examples:**
721 
722```tsx
723// ❌ Bad
724<C users={users} active={users.filter(u => u.active)} />
725<C product={product} productName={product.name} />
726 
727// ✅ Good
728<C users={users} />
729<C product={product} />
730// Do filtering/destructuring in client
731```
732 
733**Exception:** Pass derived data when transformation is expensive or client doesn't need original.
734 
735### 3.3 Cross-Request LRU Caching
736 
737**Impact: HIGH (caches across requests)**
738 
739`React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache.
740 
741**Implementation:**
742 
743```typescript
744import { LRUCache } from "lru-cache"
745 
746const cache = new LRUCache<string, any>({
747 max: 1000,
748 ttl: 5 * 60 * 1000, // 5 minutes
749})
750 
751export async function getUser(id: string) {
752 const cached = cache.get(id)
753 if (cached) return cached
754 
755 const user = await db.user.findUnique({ where: { id } })
756 cache.set(id, user)
757 return user
758}
759 
760// Request 1: DB query, result cached
761// Request 2: cache hit, no DB query
762```
763 
764Use when sequential user actions hit multiple endpoints needing the same data within seconds.
765 
766**With Vercel's [Fluid Compute](https://vercel.com/docs/fluid-compute):** LRU caching is especially effective because multiple concurrent requests can share the same function instance and cache. This means the cache persists across requests without needing external storage like Redis.
767 
768**In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching.
769 
770Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)
771 
772### 3.4 Minimize Serialization at RSC Boundaries
773 
774**Impact: HIGH (reduces data transfer size)**
775 
776The React Server/Client boundary serializes all object properties into strings and embeds them in the HTML response and subsequent RSC requests. This serialized data directly impacts page weight and load time, so **size matters a lot**. Only pass fields that the client actually uses.
777 
778**Incorrect: serializes all 50 fields**
779 
780```tsx
781async function Page() {
782 const user = await fetchUser() // 50 fields
783 return <Profile user={user} />
784}
785 
786;("use client")
787function Profile({ user }: { user: User }) {
788 return <div>{user.name}</div> // uses 1 field
789}
790```
791 
792**Correct: serializes only 1 field**
793 
794```tsx
795async function Page() {
796 const user = await fetchUser()
797 return <Profile name={user.name} />
798}
799 
800;("use client")
801function Profile({ name }: { name: string }) {
802 return <div>{name}</div>
803}
804```
805 
806### 3.5 Parallel Data Fetching with Component Composition
807 
808**Impact: CRITICAL (eliminates server-side waterfalls)**
809 
810React Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching.
811 
812**Incorrect: Sidebar waits for Page's fetch to complete**
813 
814```tsx
815export default async function Page() {
816 const header = await fetchHeader()
817 return (
818 <div>
819 <div>{header}</div>
820 <Sidebar />
821 </div>
822 )
823}
824 
825async function Sidebar() {
826 const items = await fetchSidebarItems()
827 return <nav>{items.map(renderItem)}</nav>
828}
829```
830 
831**Correct: both fetch simultaneously**
832 
833```tsx
834async function Header() {
835 const data = await fetchHeader()
836 return <div>{data}</div>
837}
838 
839async function Sidebar() {
840 const items = await fetchSidebarItems()
841 return <nav>{items.map(renderItem)}</nav>
842}
843 
844export default function Page() {
845 return (
846 <div>
847 <Header />
848 <Sidebar />
849 </div>
850 )
851}
852```
853 
854**Alternative with children prop:**
855 
856```tsx
857async function Header() {
858 const data = await fetchHeader()
859 return <div>{data}</div>
860}
861 
862async function Sidebar() {
863 const items = await fetchSidebarItems()
864 return <nav>{items.map(renderItem)}</nav>
865}
866 
867function Layout({ children }: { children: ReactNode }) {
868 return (
869 <div>
870 <Header />
871 {children}
872 </div>
873 )
874}
875 
876export default function Page() {
877 return (
878 <Layout>
879 <Sidebar />
880 </Layout>
881 )
882}
883```
884 
885### 3.6 Per-Request Deduplication with React.cache()
886 
887**Impact: MEDIUM (deduplicates within request)**
888 
889Use `React.cache()` for server-side request deduplication. Authentication and database queries benefit most.
890 
891**Usage:**
892 
893```typescript
894import { cache } from "react"
895 
896export const getCurrentUser = cache(async () => {
897 const session = await auth()
898 if (!session?.user?.id) return null
899 return await db.user.findUnique({
900 where: { id: session.user.id },
901 })
902})
903```
904 
905Within a single request, multiple calls to `getCurrentUser()` execute the query only once.
906 
907**Avoid inline objects as arguments:**
908 
909`React.cache()` uses shallow equality (`Object.is`) to determine cache hits. Inline objects create new references each call, preventing cache hits.
910 
911**Incorrect: always cache miss**
912 
913```typescript
914const getUser = cache(async (params: { uid: number }) => {
915 return await db.user.findUnique({ where: { id: params.uid } })
916})
917 
918// Each call creates new object, never hits cache
919getUser({ uid: 1 })
920getUser({ uid: 1 }) // Cache miss, runs query again
921```
922 
923**Correct: cache hit**
924 
925```typescript
926const params = { uid: 1 }
927getUser(params) // Query runs
928getUser(params) // Cache hit (same reference)
929```
930 
931If you must pass objects, pass the same reference:
932 
933**Next.js-Specific Note:**
934 
935In Next.js, the `fetch` API is automatically extended with request memoization. Requests with the same URL and options are automatically deduplicated within a single request, so you don't need `React.cache()` for `fetch` calls. However, `React.cache()` is still essential for other async tasks:
936 
937- Database queries (Prisma, Drizzle, etc.)
938 
939- Heavy computations
940 
941- Authentication checks
942 
943- File system operations
944 
945- Any non-fetch async work
946 
947Use `React.cache()` to deduplicate these operations across your component tree.
948 
949Reference: [https://react.dev/reference/react/cache](https://react.dev/reference/react/cache)
950 
951### 3.7 Use after() for Non-Blocking Operations
952 
953**Impact: MEDIUM (faster response times)**
954 
955Use Next.js's `after()` to schedule work that should execute after a response is sent. This prevents logging, analytics, and other side effects from blocking the response.
956 
957**Incorrect: blocks response**
958 
959```tsx
960import { logUserAction } from "@/app/utils"
961 
962export async function POST(request: Request) {
963 // Perform mutation
964 await updateDatabase(request)
965 
966 // Logging blocks the response
967 const userAgent = request.headers.get("user-agent") || "unknown"
968 await logUserAction({ userAgent })
969 
970 return new Response(JSON.stringify({ status: "success" }), {
971 status: 200,
972 headers: { "Content-Type": "application/json" },
973 })
974}
975```
976 
977**Correct: non-blocking**
978 
979```tsx
980import { after } from "next/server"
981import { headers, cookies } from "next/headers"
982import { logUserAction } from "@/app/utils"
983 
984export async function POST(request: Request) {
985 // Perform mutation
986 await updateDatabase(request)
987 
988 // Log after response is sent
989 after(async () => {
990 const userAgent = (await headers()).get("user-agent") || "unknown"
991 const sessionCookie = (await cookies()).get("session-id")?.value || "anonymous"
992 
993 logUserAction({ sessionCookie, userAgent })
994 })
995 
996 return new Response(JSON.stringify({ status: "success" }), {
997 status: 200,
998 headers: { "Content-Type": "application/json" },
999 })
1000}
1001```
1002 
1003The response is sent immediately while logging happens in the background.
1004 
1005**Common use cases:**
1006 
1007- Analytics tracking
1008 
1009- Audit logging
1010 
1011- Sending notifications
1012 
1013- Cache invalidation
1014 
1015- Cleanup tasks
1016 
1017**Important notes:**
1018 
1019- `after()` runs even if the response fails or redirects
1020 
1021- Works in Server Actions, Route Handlers, and Server Components
1022 
1023Reference: [https://nextjs.org/docs/app/api-reference/functions/after](https://nextjs.org/docs/app/api-reference/functions/after)
1024 
1025---
1026 
1027## 4. Client-Side Data Fetching
1028 
1029**Impact: MEDIUM-HIGH**
1030 
1031Automatic deduplication and efficient data fetching patterns reduce redundant network requests.
1032 
1033### 4.1 Deduplicate Global Event Listeners
1034 
1035**Impact: LOW (single listener for N components)**
1036 
1037Use `useSWRSubscription()` to share global event listeners across component instances.
1038 
1039**Incorrect: N instances = N listeners**
1040 
1041```tsx
1042function useKeyboardShortcut(key: string, callback: () => void) {
1043 useEffect(() => {
1044 const handler = (e: KeyboardEvent) => {
1045 if (e.metaKey && e.key === key) {
1046 callback()
1047 }
1048 }
1049 window.addEventListener("keydown", handler)
1050 return () => window.removeEventListener("keydown", handler)
1051 }, [key, callback])
1052}
1053```
1054 
1055When using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener.
1056 
1057**Correct: N instances = 1 listener**
1058 
1059```tsx
1060import useSWRSubscription from "swr/subscription"
1061 
1062// Module-level Map to track callbacks per key
1063const keyCallbacks = new Map<string, Set<() => void>>()
1064 
1065function useKeyboardShortcut(key: string, callback: () => void) {
1066 // Register this callback in the Map
1067 useEffect(() => {
1068 if (!keyCallbacks.has(key)) {
1069 keyCallbacks.set(key, new Set())
1070 }
1071 keyCallbacks.get(key)!.add(callback)
1072 
1073 return () => {
1074 const set = keyCallbacks.get(key)
1075 if (set) {
1076 set.delete(callback)
1077 if (set.size === 0) {
1078 keyCallbacks.delete(key)
1079 }
1080 }
1081 }
1082 }, [key, callback])
1083 
1084 useSWRSubscription("global-keydown", () => {
1085 const handler = (e: KeyboardEvent) => {
1086 if (e.metaKey && keyCallbacks.has(e.key)) {
1087 keyCallbacks.get(e.key)!.forEach((cb) => cb())
1088 }
1089 }
1090 window.addEventListener("keydown", handler)
1091 return () => window.removeEventListener("keydown", handler)
1092 })
1093}
1094 
1095function Profile() {
1096 // Multiple shortcuts will share the same listener
1097 useKeyboardShortcut("p", () => {
1098 /* ... */
1099 })
1100 useKeyboardShortcut("k", () => {
1101 /* ... */
1102 })
1103 // ...
1104}
1105```
1106 
1107### 4.2 Use Passive Event Listeners for Scrolling Performance
1108 
1109**Impact: MEDIUM (eliminates scroll delay caused by event listeners)**
1110 
1111Add `{ passive: true }` to touch and wheel event listeners to enable immediate scrolling. Browsers normally wait for listeners to finish to check if `preventDefault()` is called, causing scroll delay.
1112 
1113**Incorrect:**
1114 
1115```typescript
1116useEffect(() => {
1117 const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)
1118 const handleWheel = (e: WheelEvent) => console.log(e.deltaY)
1119 
1120 document.addEventListener("touchstart", handleTouch)
1121 document.addEventListener("wheel", handleWheel)
1122 
1123 return () => {
1124 document.removeEventListener("touchstart", handleTouch)
1125 document.removeEventListener("wheel", handleWheel)
1126 }
1127}, [])
1128```
1129 
1130**Correct:**
1131 
1132```typescript
1133useEffect(() => {
1134 const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)
1135 const handleWheel = (e: WheelEvent) => console.log(e.deltaY)
1136 
1137 document.addEventListener("touchstart", handleTouch, { passive: true })
1138 document.addEventListener("wheel", handleWheel, { passive: true })
1139 
1140 return () => {
1141 document.removeEventListener("touchstart", handleTouch)
1142 document.removeEventListener("wheel", handleWheel)
1143 }
1144}, [])
1145```
1146 
1147**Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`.
1148 
1149**Don't use passive when:** implementing custom swipe gestures, custom zoom controls, or any listener that needs `preventDefault()`.
1150 
1151### 4.3 Use SWR for Automatic Deduplication
1152 
1153**Impact: MEDIUM-HIGH (automatic deduplication)**
1154 
1155SWR enables request deduplication, caching, and revalidation across component instances.
1156 
1157**Incorrect: no deduplication, each instance fetches**
1158 
1159```tsx
1160function UserList() {
1161 const [users, setUsers] = useState([])
1162 useEffect(() => {
1163 fetch("/api/users")
1164 .then((r) => r.json())
1165 .then(setUsers)
1166 }, [])
1167}
1168```
1169 
1170**Correct: multiple instances share one request**
1171 
1172```tsx
1173import useSWR from "swr"
1174 
1175function UserList() {
1176 const { data: users } = useSWR("/api/users", fetcher)
1177}
1178```
1179 
1180**For immutable data:**
1181 
1182```tsx
1183import { useImmutableSWR } from "@/lib/swr"
1184 
1185function StaticContent() {
1186 const { data } = useImmutableSWR("/api/config", fetcher)
1187}
1188```
1189 
1190**For mutations:**
1191 
1192```tsx
1193import { useSWRMutation } from "swr/mutation"
1194 
1195function UpdateButton() {
1196 const { trigger } = useSWRMutation("/api/user", updateUser)
1197 return <button onClick={() => trigger()}>Update</button>
1198}
1199```
1200 
1201Reference: [https://swr.vercel.app](https://swr.vercel.app)
1202 
1203### 4.4 Version and Minimize localStorage Data
1204 
1205**Impact: MEDIUM (prevents schema conflicts, reduces storage size)**
1206 
1207Add version prefix to keys and store only needed fields. Prevents schema conflicts and accidental storage of sensitive data.
1208 
1209**Incorrect:**
1210 
1211```typescript
1212// No version, stores everything, no error handling
1213localStorage.setItem("userConfig", JSON.stringify(fullUserObject))
1214const data = localStorage.getItem("userConfig")
1215```
1216 
1217**Correct:**
1218 
1219```typescript
1220const VERSION = "v2"
1221 
1222function saveConfig(config: { theme: string; language: string }) {
1223 try {
1224 localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config))
1225 } catch {
1226 // Throws in incognito/private browsing, quota exceeded, or disabled
1227 }
1228}
1229 
1230function loadConfig() {
1231 try {
1232 const data = localStorage.getItem(`userConfig:${VERSION}`)
1233 return data ? JSON.parse(data) : null
1234 } catch {
1235 return null
1236 }
1237}
1238 
1239// Migration from v1 to v2
1240function migrate() {
1241 try {
1242 const v1 = localStorage.getItem("userConfig:v1")
1243 if (v1) {
1244 const old = JSON.parse(v1)
1245 saveConfig({ theme: old.darkMode ? "dark" : "light", language: old.lang })
1246 localStorage.removeItem("userConfig:v1")
1247 }
1248 } catch {}
1249}
1250```
1251 
1252**Store minimal fields from server responses:**
1253 
1254```typescript
1255// User object has 20+ fields, only store what UI needs
1256function cachePrefs(user: FullUser) {
1257 try {
1258 localStorage.setItem(
1259 "prefs:v1",
1260 JSON.stringify({
1261 theme: user.preferences.theme,
1262 notifications: user.preferences.notifications,
1263 })
1264 )
1265 } catch {}
1266}
1267```
1268 
1269**Always wrap in try-catch:** `getItem()` and `setItem()` throw in incognito/private browsing (Safari, Firefox), when quota exceeded, or when disabled.
1270 
1271**Benefits:** Schema evolution via versioning, reduced storage size, prevents storing tokens/PII/internal flags.
1272 
1273---
1274 
1275## 5. Re-render Optimization
1276 
1277**Impact: MEDIUM**
1278 
1279Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness.
1280 
1281### 5.1 Calculate Derived State During Rendering
1282 
1283**Impact: MEDIUM (avoids redundant renders and state drift)**
1284 
1285If a value can be computed from current props/state, do not store it in state or update it in an effect. Derive it during render to avoid extra renders and state drift. Do not set state in effects solely in response to prop changes; prefer derived values or keyed resets instead.
1286 
1287**Incorrect: redundant state and effect**
1288 
1289```tsx
1290function Form() {
1291 const [firstName, setFirstName] = useState("First")
1292 const [lastName, setLastName] = useState("Last")
1293 const [fullName, setFullName] = useState("")
1294 
1295 useEffect(() => {
1296 setFullName(firstName + " " + lastName)
1297 }, [firstName, lastName])
1298 
1299 return <p>{fullName}</p>
1300}
1301```
1302 
1303**Correct: derive during render**
1304 
1305```tsx
1306function Form() {
1307 const [firstName, setFirstName] = useState("First")
1308 const [lastName, setLastName] = useState("Last")
1309 const fullName = firstName + " " + lastName
1310 
1311 return <p>{fullName}</p>
1312}
1313```
1314 
1315Reference: [https://react.dev/learn/you-might-not-need-an-effect](https://react.dev/learn/you-might-not-need-an-effect)
1316 
1317### 5.2 Defer State Reads to Usage Point
1318 
1319**Impact: MEDIUM (avoids unnecessary subscriptions)**
1320 
1321Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks.
1322 
1323**Incorrect: subscribes to all searchParams changes**
1324 
1325```tsx
1326function ShareButton({ chatId }: { chatId: string }) {
1327 const searchParams = useSearchParams()
1328 
1329 const handleShare = () => {
1330 const ref = searchParams.get("ref")
1331 shareChat(chatId, { ref })
1332 }
1333 
1334 return <button onClick={handleShare}>Share</button>
1335}
1336```
1337 
1338**Correct: reads on demand, no subscription**
1339 
1340```tsx
1341function ShareButton({ chatId }: { chatId: string }) {
1342 const handleShare = () => {
1343 const params = new URLSearchParams(window.location.search)
1344 const ref = params.get("ref")
1345 shareChat(chatId, { ref })
1346 }
1347 
1348 return <button onClick={handleShare}>Share</button>
1349}
1350```
1351 
1352### 5.3 Do not wrap a simple expression with a primitive result type in useMemo
1353 
1354**Impact: LOW-MEDIUM (wasted computation on every render)**
1355 
1356When an expression is simple (few logical or arithmetical operators) and has a primitive result type (boolean, number, string), do not wrap it in `useMemo`.
1357 
1358Calling `useMemo` and comparing hook dependencies may consume more resources than the expression itself.
1359 
1360**Incorrect:**
1361 
1362```tsx
1363function Header({ user, notifications }: Props) {
1364 const isLoading = useMemo(() => {
1365 return user.isLoading || notifications.isLoading
1366 }, [user.isLoading, notifications.isLoading])
1367 
1368 if (isLoading) return <Skeleton />
1369 // return some markup
1370}
1371```
1372 
1373**Correct:**
1374 
1375```tsx
1376function Header({ user, notifications }: Props) {
1377 const isLoading = user.isLoading || notifications.isLoading
1378 
1379 if (isLoading) return <Skeleton />
1380 // return some markup
1381}
1382```
1383 
1384### 5.4 Extract Default Non-primitive Parameter Value from Memoized Component to Constant
1385 
1386**Impact: MEDIUM (restores memoization by using a constant for default value)**
1387 
1388When memoized component has a default value for some non-primitive optional parameter, such as an array, function, or object, calling the component without that parameter results in broken memoization. This is because new value instances are created on every rerender, and they do not pass strict equality comparison in `memo()`.
1389 
1390To address this issue, extract the default value into a constant.
1391 
1392**Incorrect: `onClick` has different values on every rerender**
1393 
1394```tsx
1395const UserAvatar = memo(function UserAvatar({ onClick = () => {} }: { onClick?: () => void }) {
1396 // ...
1397})
1398 
1399// Used without optional onClick
1400<UserAvatar />
1401```
1402 
1403**Correct: stable default value**
1404 
1405```tsx
1406const NOOP = () => {};
1407 
1408const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) {
1409 // ...
1410})
1411 
1412// Used without optional onClick
1413<UserAvatar />
1414```
1415 
1416### 5.5 Extract to Memoized Components
1417 
1418**Impact: MEDIUM (enables early returns)**
1419 
1420Extract expensive work into memoized components to enable early returns before computation.
1421 
1422**Incorrect: computes avatar even when loading**
1423 
1424```tsx
1425function Profile({ user, loading }: Props) {
1426 const avatar = useMemo(() => {
1427 const id = computeAvatarId(user)
1428 return <Avatar id={id} />
1429 }, [user])
1430 
1431 if (loading) return <Skeleton />
1432 return <div>{avatar}</div>
1433}
1434```
1435 
1436**Correct: skips computation when loading**
1437 
1438```tsx
1439const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
1440 const id = useMemo(() => computeAvatarId(user), [user])
1441 return <Avatar id={id} />
1442})
1443 
1444function Profile({ user, loading }: Props) {
1445 if (loading) return <Skeleton />
1446 return (
1447 <div>
1448 <UserAvatar user={user} />
1449 </div>
1450 )
1451}
1452```
1453 
1454**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, manual memoization with `memo()` and `useMemo()` is not necessary. The compiler automatically optimizes re-renders.
1455 
1456### 5.6 Narrow Effect Dependencies
1457 
1458**Impact: LOW (minimizes effect re-runs)**
1459 
1460Specify primitive dependencies instead of objects to minimize effect re-runs.
1461 
1462**Incorrect: re-runs on any user field change**
1463 
1464```tsx
1465useEffect(() => {
1466 console.log(user.id)
1467}, [user])
1468```
1469 
1470**Correct: re-runs only when id changes**
1471 
1472```tsx
1473useEffect(() => {
1474 console.log(user.id)
1475}, [user.id])
1476```
1477 
1478**For derived state, compute outside effect:**
1479 
1480```tsx
1481// Incorrect: runs on width=767, 766, 765...
1482useEffect(() => {
1483 if (width < 768) {
1484 enableMobileMode()
1485 }
1486}, [width])
1487 
1488// Correct: runs only on boolean transition
1489const isMobile = width < 768
1490useEffect(() => {
1491 if (isMobile) {
1492 enableMobileMode()
1493 }
1494}, [isMobile])
1495```
1496 
1497### 5.7 Put Interaction Logic in Event Handlers
1498 
1499**Impact: MEDIUM (avoids effect re-runs and duplicate side effects)**
1500 
1501If a side effect is triggered by a specific user action (submit, click, drag), run it in that event handler. Do not model the action as state + effect; it makes effects re-run on unrelated changes and can duplicate the action.
1502 
1503**Incorrect: event modeled as state + effect**
1504 
1505```tsx
1506function Form() {
1507 const [submitted, setSubmitted] = useState(false)
1508 const theme = useContext(ThemeContext)
1509 
1510 useEffect(() => {
1511 if (submitted) {
1512 post("/api/register")
1513 showToast("Registered", theme)
1514 }
1515 }, [submitted, theme])
1516 
1517 return <button onClick={() => setSubmitted(true)}>Submit</button>
1518}
1519```
1520 
1521**Correct: do it in the handler**
1522 
1523```tsx
1524function Form() {
1525 const theme = useContext(ThemeContext)
1526 
1527 function handleSubmit() {
1528 post("/api/register")
1529 showToast("Registered", theme)
1530 }
1531 
1532 return <button onClick={handleSubmit}>Submit</button>
1533}
1534```
1535 
1536Reference: [https://react.dev/learn/removing-effect-dependencies#should-this-code-move-to-an-event-handler](https://react.dev/learn/removing-effect-dependencies#should-this-code-move-to-an-event-handler)
1537 
1538### 5.8 Subscribe to Derived State
1539 
1540**Impact: MEDIUM (reduces re-render frequency)**
1541 
1542Subscribe to derived boolean state instead of continuous values to reduce re-render frequency.
1543 
1544**Incorrect: re-renders on every pixel change**
1545 
1546```tsx
1547function Sidebar() {
1548 const width = useWindowWidth() // updates continuously
1549 const isMobile = width < 768
1550 return <nav className={isMobile ? "mobile" : "desktop"} />
1551}
1552```
1553 
1554**Correct: re-renders only when boolean changes**
1555 
1556```tsx
1557function Sidebar() {
1558 const isMobile = useMediaQuery("(max-width: 767px)")
1559 return <nav className={isMobile ? "mobile" : "desktop"} />
1560}
1561```
1562 
1563### 5.9 Use Functional setState Updates
1564 
1565**Impact: MEDIUM (prevents stale closures and unnecessary callback recreations)**
1566 
1567When updating state based on the current state value, use the functional update form of setState instead of directly referencing the state variable. This prevents stale closures, eliminates unnecessary dependencies, and creates stable callback references.
1568 
1569**Incorrect: requires state as dependency**
1570 
1571```tsx
1572function TodoList() {
1573 const [items, setItems] = useState(initialItems)
1574 
1575 // Callback must depend on items, recreated on every items change
1576 const addItems = useCallback(
1577 (newItems: Item[]) => {
1578 setItems([...items, ...newItems])
1579 },
1580 [items]
1581 ) // ❌ items dependency causes recreations
1582 
1583 // Risk of stale closure if dependency is forgotten
1584 const removeItem = useCallback((id: string) => {
1585 setItems(items.filter((item) => item.id !== id))
1586 }, []) // ❌ Missing items dependency - will use stale items!
1587 
1588 return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
1589}
1590```
1591 
1592The first callback is recreated every time `items` changes, which can cause child components to re-render unnecessarily. The second callback has a stale closure bug—it will always reference the initial `items` value.
1593 
1594**Correct: stable callbacks, no stale closures**
1595 
1596```tsx
1597function TodoList() {
1598 const [items, setItems] = useState(initialItems)
1599 
1600 // Stable callback, never recreated
1601 const addItems = useCallback((newItems: Item[]) => {
1602 setItems((curr) => [...curr, ...newItems])
1603 }, []) // ✅ No dependencies needed
1604 
1605 // Always uses latest state, no stale closure risk
1606 const removeItem = useCallback((id: string) => {
1607 setItems((curr) => curr.filter((item) => item.id !== id))
1608 }, []) // ✅ Safe and stable
1609 
1610 return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
1611}
1612```
1613 
1614**Benefits:**
1615 
16161. **Stable callback references** - Callbacks don't need to be recreated when state changes
1617 
16182. **No stale closures** - Always operates on the latest state value
1619 
16203. **Fewer dependencies** - Simplifies dependency arrays and reduces memory leaks
1621 
16224. **Prevents bugs** - Eliminates the most common source of React closure bugs
1623 
1624**When to use functional updates:**
1625 
1626- Any setState that depends on the current state value
1627 
1628- Inside useCallback/useMemo when state is needed
1629 
1630- Event handlers that reference state
1631 
1632- Async operations that update state
1633 
1634**When direct updates are fine:**
1635 
1636- Setting state to a static value: `setCount(0)`
1637 
1638- Setting state from props/arguments only: `setName(newName)`
1639 
1640- State doesn't depend on previous value
1641 
1642**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler can automatically optimize some cases, but functional updates are still recommended for correctness and to prevent stale closure bugs.
1643 
1644### 5.10 Use Lazy State Initialization
1645 
1646**Impact: MEDIUM (wasted computation on every render)**
1647 
1648Pass a function to `useState` for expensive initial values. Without the function form, the initializer runs on every render even though the value is only used once.
1649 
1650**Incorrect: runs on every render**
1651 
1652```tsx
1653function FilteredList({ items }: { items: Item[] }) {
1654 // buildSearchIndex() runs on EVERY render, even after initialization
1655 const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))
1656 const [query, setQuery] = useState("")
1657 
1658 // When query changes, buildSearchIndex runs again unnecessarily
1659 return <SearchResults index={searchIndex} query={query} />
1660}
1661 
1662function UserProfile() {
1663 // JSON.parse runs on every render
1664 const [settings, setSettings] = useState(JSON.parse(localStorage.getItem("settings") || "{}"))
1665 
1666 return <SettingsForm settings={settings} onChange={setSettings} />
1667}
1668```
1669 
1670**Correct: runs only once**
1671 
1672```tsx
1673function FilteredList({ items }: { items: Item[] }) {
1674 // buildSearchIndex() runs ONLY on initial render
1675 const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))
1676 const [query, setQuery] = useState("")
1677 
1678 return <SearchResults index={searchIndex} query={query} />
1679}
1680 
1681function UserProfile() {
1682 // JSON.parse runs only on initial render
1683 const [settings, setSettings] = useState(() => {
1684 const stored = localStorage.getItem("settings")
1685 return stored ? JSON.parse(stored) : {}
1686 })
1687 
1688 return <SettingsForm settings={settings} onChange={setSettings} />
1689}
1690```
1691 
1692Use lazy initialization when computing initial values from localStorage/sessionStorage, building data structures (indexes, maps), reading from the DOM, or performing heavy transformations.
1693 
1694For simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary.
1695 
1696### 5.11 Use Transitions for Non-Urgent Updates
1697 
1698**Impact: MEDIUM (maintains UI responsiveness)**
1699 
1700Mark frequent, non-urgent state updates as transitions to maintain UI responsiveness.
1701 
1702**Incorrect: blocks UI on every scroll**
1703 
1704```tsx
1705function ScrollTracker() {
1706 const [scrollY, setScrollY] = useState(0)
1707 useEffect(() => {
1708 const handler = () => setScrollY(window.scrollY)
1709 window.addEventListener("scroll", handler, { passive: true })
1710 return () => window.removeEventListener("scroll", handler)
1711 }, [])
1712}
1713```
1714 
1715**Correct: non-blocking updates**
1716 
1717```tsx
1718import { startTransition } from "react"
1719 
1720function ScrollTracker() {
1721 const [scrollY, setScrollY] = useState(0)
1722 useEffect(() => {
1723 const handler = () => {
1724 startTransition(() => setScrollY(window.scrollY))
1725 }
1726 window.addEventListener("scroll", handler, { passive: true })
1727 return () => window.removeEventListener("scroll", handler)
1728 }, [])
1729}
1730```
1731 
1732### 5.12 Use useRef for Transient Values
1733 
1734**Impact: MEDIUM (avoids unnecessary re-renders on frequent updates)**
1735 
1736When a value changes frequently and you don't want a re-render on every update (e.g., mouse trackers, intervals, transient flags), store it in `useRef` instead of `useState`. Keep component state for UI; use refs for temporary DOM-adjacent values. Updating a ref does not trigger a re-render.
1737 
1738**Incorrect: renders every update**
1739 
1740```tsx
1741function Tracker() {
1742 const [lastX, setLastX] = useState(0)
1743 
1744 useEffect(() => {
1745 const onMove = (e: MouseEvent) => setLastX(e.clientX)
1746 window.addEventListener("mousemove", onMove)
1747 return () => window.removeEventListener("mousemove", onMove)
1748 }, [])
1749 
1750 return (
1751 <div
1752 style={{
1753 position: "fixed",
1754 top: 0,
1755 left: lastX,
1756 width: 8,
1757 height: 8,
1758 background: "black",
1759 }}
1760 />
1761 )
1762}
1763```
1764 
1765**Correct: no re-render for tracking**
1766 
1767```tsx
1768function Tracker() {
1769 const lastXRef = useRef(0)
1770 const dotRef = useRef<HTMLDivElement>(null)
1771 
1772 useEffect(() => {
1773 const onMove = (e: MouseEvent) => {
1774 lastXRef.current = e.clientX
1775 const node = dotRef.current
1776 if (node) {
1777 node.style.transform = `translateX(${e.clientX}px)`
1778 }
1779 }
1780 window.addEventListener("mousemove", onMove)
1781 return () => window.removeEventListener("mousemove", onMove)
1782 }, [])
1783 
1784 return (
1785 <div
1786 ref={dotRef}
1787 style={{
1788 position: "fixed",
1789 top: 0,
1790 left: 0,
1791 width: 8,
1792 height: 8,
1793 background: "black",
1794 transform: "translateX(0px)",
1795 }}
1796 />
1797 )
1798}
1799```
1800 
1801---
1802 
1803## 6. Rendering Performance
1804 
1805**Impact: MEDIUM**
1806 
1807Optimizing the rendering process reduces the work the browser needs to do.
1808 
1809### 6.1 Animate SVG Wrapper Instead of SVG Element
1810 
1811**Impact: LOW (enables hardware acceleration)**
1812 
1813Many browsers don't have hardware acceleration for CSS3 animations on SVG elements. Wrap SVG in a `<div>` and animate the wrapper instead.
1814 
1815**Incorrect: animating SVG directly - no hardware acceleration**
1816 
1817```tsx
1818function LoadingSpinner() {
1819 return (
1820 <svg className="animate-spin" width="24" height="24" viewBox="0 0 24 24">
1821 <circle cx="12" cy="12" r="10" stroke="currentColor" />
1822 </svg>
1823 )
1824}
1825```
1826 
1827**Correct: animating wrapper div - hardware accelerated**
1828 
1829```tsx
1830function LoadingSpinner() {
1831 return (
1832 <div className="animate-spin">
1833 <svg width="24" height="24" viewBox="0 0 24 24">
1834 <circle cx="12" cy="12" r="10" stroke="currentColor" />
1835 </svg>
1836 </div>
1837 )
1838}
1839```
1840 
1841This applies to all CSS transforms and transitions (`transform`, `opacity`, `translate`, `scale`, `rotate`). The wrapper div allows browsers to use GPU acceleration for smoother animations.
1842 
1843### 6.2 CSS content-visibility for Long Lists
1844 
1845**Impact: HIGH (faster initial render)**
1846 
1847Apply `content-visibility: auto` to defer off-screen rendering.
1848 
1849**CSS:**
1850 
1851```css
1852.message-item {
1853 content-visibility: auto;
1854 contain-intrinsic-size: 0 80px;
1855}
1856```
1857 
1858**Example:**
1859 
1860```tsx
1861function MessageList({ messages }: { messages: Message[] }) {
1862 return (
1863 <div className="h-screen overflow-y-auto">
1864 {messages.map((msg) => (
1865 <div key={msg.id} className="message-item">
1866 <Avatar user={msg.author} />
1867 <div>{msg.content}</div>
1868 </div>
1869 ))}
1870 </div>
1871 )
1872}
1873```
1874 
1875For 1000 messages, browser skips layout/paint for ~990 off-screen items (10× faster initial render).
1876 
1877### 6.3 Hoist Static JSX Elements
1878 
1879**Impact: LOW (avoids re-creation)**
1880 
1881Extract static JSX outside components to avoid re-creation.
1882 
1883**Incorrect: recreates element every render**
1884 
1885```tsx
1886function LoadingSkeleton() {
1887 return <div className="h-20 animate-pulse bg-gray-200" />
1888}
1889 
1890function Container() {
1891 return <div>{loading && <LoadingSkeleton />}</div>
1892}
1893```
1894 
1895**Correct: reuses same element**
1896 
1897```tsx
1898const loadingSkeleton = <div className="h-20 animate-pulse bg-gray-200" />
1899 
1900function Container() {
1901 return <div>{loading && loadingSkeleton}</div>
1902}
1903```
1904 
1905This is especially helpful for large and static SVG nodes, which can be expensive to recreate on every render.
1906 
1907**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler automatically hoists static JSX elements and optimizes component re-renders, making manual hoisting unnecessary.
1908 
1909### 6.4 Optimize SVG Precision
1910 
1911**Impact: LOW (reduces file size)**
1912 
1913Reduce SVG coordinate precision to decrease file size. The optimal precision depends on the viewBox size, but in general reducing precision should be considered.
1914 
1915**Incorrect: excessive precision**
1916 
1917```svg
1918<path d="M 10.293847 20.847362 L 30.938472 40.192837" />
1919```
1920 
1921**Correct: 1 decimal place**
1922 
1923```svg
1924<path d="M 10.3 20.8 L 30.9 40.2" />
1925```
1926 
1927**Automate with SVGO:**
1928 
1929```bash
1930npx svgo --precision=1 --multipass icon.svg
1931```
1932 
1933### 6.5 Prevent Hydration Mismatch Without Flickering
1934 
1935**Impact: MEDIUM (avoids visual flicker and hydration errors)**
1936 
1937When rendering content that depends on client-side storage (localStorage, cookies), avoid both SSR breakage and post-hydration flickering by injecting a synchronous script that updates the DOM before React hydrates.
1938 
1939**Incorrect: breaks SSR**
1940 
1941```tsx
1942function ThemeWrapper({ children }: { children: ReactNode }) {
1943 // localStorage is not available on server - throws error
1944 const theme = localStorage.getItem("theme") || "light"
1945 
1946 return <div className={theme}>{children}</div>
1947}
1948```
1949 
1950Server-side rendering will fail because `localStorage` is undefined.
1951 
1952**Incorrect: visual flickering**
1953 
1954```tsx
1955function ThemeWrapper({ children }: { children: ReactNode }) {
1956 const [theme, setTheme] = useState("light")
1957 
1958 useEffect(() => {
1959 // Runs after hydration - causes visible flash
1960 const stored = localStorage.getItem("theme")
1961 if (stored) {
1962 setTheme(stored)
1963 }
1964 }, [])
1965 
1966 return <div className={theme}>{children}</div>
1967}
1968```
1969 
1970Component first renders with default value (`light`), then updates after hydration, causing a visible flash of incorrect content.
1971 
1972**Correct: no flicker, no hydration mismatch**
1973 
1974```tsx
1975function ThemeWrapper({ children }: { children: ReactNode }) {
1976 return (
1977 <>
1978 <div id="theme-wrapper">{children}</div>
1979 <script
1980 dangerouslySetInnerHTML={{
1981 __html: `
1982 (function() {
1983 try {
1984 var theme = localStorage.getItem('theme') || 'light';
1985 var el = document.getElementById('theme-wrapper');
1986 if (el) el.className = theme;
1987 } catch (e) {}
1988 })();
1989 `,
1990 }}
1991 />
1992 </>
1993 )
1994}
1995```
1996 
1997The inline script executes synchronously before showing the element, ensuring the DOM already has the correct value. No flickering, no hydration mismatch.
1998 
1999This pattern is especially useful for theme toggles, user preferences, authentication states, and any client-only data that should render immediately without flashing default values.
2000 
2001### 6.6 Suppress Expected Hydration Mismatches
2002 
2003**Impact: LOW-MEDIUM (avoids noisy hydration warnings for known differences)**
2004 
2005In SSR frameworks (e.g., Next.js), some values are intentionally different on server vs client (random IDs, dates, locale/timezone formatting). For these _expected_ mismatches, wrap the dynamic text in an element with `suppressHydrationWarning` to prevent noisy warnings. Do not use this to hide real bugs. Don’t overuse it.
2006 
2007**Incorrect: known mismatch warnings**
2008 
2009```tsx
2010function Timestamp() {
2011 return <span>{new Date().toLocaleString()}</span>
2012}
2013```
2014 
2015**Correct: suppress expected mismatch only**
2016 
2017```tsx
2018function Timestamp() {
2019 return <span suppressHydrationWarning>{new Date().toLocaleString()}</span>
2020}
2021```
2022 
2023### 6.7 Use Activity Component for Show/Hide
2024 
2025**Impact: MEDIUM (preserves state/DOM)**
2026 
2027Use React's `<Activity>` to preserve state/DOM for expensive components that frequently toggle visibility.
2028 
2029**Usage:**
2030 
2031```tsx
2032import { Activity } from "react"
2033 
2034function Dropdown({ isOpen }: Props) {
2035 return (
2036 <Activity mode={isOpen ? "visible" : "hidden"}>
2037 <ExpensiveMenu />
2038 </Activity>
2039 )
2040}
2041```
2042 
2043Avoids expensive re-renders and state loss.
2044 
2045### 6.8 Use Explicit Conditional Rendering
2046 
2047**Impact: LOW (prevents rendering 0 or NaN)**
2048 
2049Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render.
2050 
2051**Incorrect: renders "0" when count is 0**
2052 
2053```tsx
2054function Badge({ count }: { count: number }) {
2055 return <div>{count && <span className="badge">{count}</span>}</div>
2056}
2057 
2058// When count = 0, renders: <div>0</div>
2059// When count = 5, renders: <div><span class="badge">5</span></div>
2060```
2061 
2062**Correct: renders nothing when count is 0**
2063 
2064```tsx
2065function Badge({ count }: { count: number }) {
2066 return <div>{count > 0 ? <span className="badge">{count}</span> : null}</div>
2067}
2068 
2069// When count = 0, renders: <div></div>
2070// When count = 5, renders: <div><span class="badge">5</span></div>
2071```
2072 
2073### 6.9 Use useTransition Over Manual Loading States
2074 
2075**Impact: LOW (reduces re-renders and improves code clarity)**
2076 
2077Use `useTransition` instead of manual `useState` for loading states. This provides built-in `isPending` state and automatically manages transitions.
2078 
2079**Incorrect: manual loading state**
2080 
2081```tsx
2082function SearchResults() {
2083 const [query, setQuery] = useState("")
2084 const [results, setResults] = useState([])
2085 const [isLoading, setIsLoading] = useState(false)
2086 
2087 const handleSearch = async (value: string) => {
2088 setIsLoading(true)
2089 setQuery(value)
2090 const data = await fetchResults(value)
2091 setResults(data)
2092 setIsLoading(false)
2093 }
2094 
2095 return (
2096 <>
2097 <input onChange={(e) => handleSearch(e.target.value)} />
2098 {isLoading && <Spinner />}
2099 <ResultsList results={results} />
2100 </>
2101 )
2102}
2103```
2104 
2105**Correct: useTransition with built-in pending state**
2106 
2107```tsx
2108import { useTransition, useState } from "react"
2109 
2110function SearchResults() {
2111 const [query, setQuery] = useState("")
2112 const [results, setResults] = useState([])
2113 const [isPending, startTransition] = useTransition()
2114 
2115 const handleSearch = (value: string) => {
2116 setQuery(value) // Update input immediately
2117 
2118 startTransition(async () => {
2119 // Fetch and update results
2120 const data = await fetchResults(value)
2121 setResults(data)
2122 })
2123 }
2124 
2125 return (
2126 <>
2127 <input onChange={(e) => handleSearch(e.target.value)} />
2128 {isPending && <Spinner />}
2129 <ResultsList results={results} />
2130 </>
2131 )
2132}
2133```
2134 
2135**Benefits:**
2136 
2137- **Automatic pending state**: No need to manually manage `setIsLoading(true/false)`
2138 
2139- **Error resilience**: Pending state correctly resets even if the transition throws
2140 
2141- **Better responsiveness**: Keeps the UI responsive during updates
2142 
2143- **Interrupt handling**: New transitions automatically cancel pending ones
2144 
2145Reference: [https://react.dev/reference/react/useTransition](https://react.dev/reference/react/useTransition)
2146 
2147---
2148 
2149## 7. JavaScript Performance
2150 
2151**Impact: LOW-MEDIUM**
2152 
2153Micro-optimizations for hot paths can add up to meaningful improvements.
2154 
2155### 7.1 Avoid Layout Thrashing
2156 
2157**Impact: MEDIUM (prevents forced synchronous layouts and reduces performance bottlenecks)**
2158 
2159Avoid interleaving style writes with layout reads. When you read a layout property (like `offsetWidth`, `getBoundingClientRect()`, or `getComputedStyle()`) between style changes, the browser is forced to trigger a synchronous reflow.
2160 
2161**This is OK: browser batches style changes**
2162 
2163```typescript
2164function updateElementStyles(element: HTMLElement) {
2165 // Each line invalidates style, but browser batches the recalculation
2166 element.style.width = "100px"
2167 element.style.height = "200px"
2168 element.style.backgroundColor = "blue"
2169 element.style.border = "1px solid black"
2170}
2171```
2172 
2173**Incorrect: interleaved reads and writes force reflows**
2174 
2175```typescript
2176function layoutThrashing(element: HTMLElement) {
2177 element.style.width = "100px"
2178 const width = element.offsetWidth // Forces reflow
2179 element.style.height = "200px"
2180 const height = element.offsetHeight // Forces another reflow
2181}
2182```
2183 
2184**Correct: batch writes, then read once**
2185 
2186```typescript
2187function updateElementStyles(element: HTMLElement) {
2188 // Batch all writes together
2189 element.style.width = "100px"
2190 element.style.height = "200px"
2191 element.style.backgroundColor = "blue"
2192 element.style.border = "1px solid black"
2193 
2194 // Read after all writes are done (single reflow)
2195 const { width, height } = element.getBoundingClientRect()
2196}
2197```
2198 
2199**Correct: batch reads, then writes**
2200 
2201```typescript
2202function updateElementStyles(element: HTMLElement) {
2203 element.classList.add("highlighted-box")
2204 
2205 const { width, height } = element.getBoundingClientRect()
2206}
2207```
2208 
2209**Better: use CSS classes**
2210 
2211**React example:**
2212 
2213```tsx
2214// Incorrect: interleaving style changes with layout queries
2215function Box({ isHighlighted }: { isHighlighted: boolean }) {
2216 const ref = useRef<HTMLDivElement>(null)
2217 
2218 useEffect(() => {
2219 if (ref.current && isHighlighted) {
2220 ref.current.style.width = "100px"
2221 const width = ref.current.offsetWidth // Forces layout
2222 ref.current.style.height = "200px"
2223 }
2224 }, [isHighlighted])
2225 
2226 return <div ref={ref}>Content</div>
2227}
2228 
2229// Correct: toggle class
2230function Box({ isHighlighted }: { isHighlighted: boolean }) {
2231 return <div className={isHighlighted ? "highlighted-box" : ""}>Content</div>
2232}
2233```
2234 
2235Prefer CSS classes over inline styles when possible. CSS files are cached by the browser, and classes provide better separation of concerns and are easier to maintain.
2236 
2237See [this gist](https://gist.github.com/paulirish/5d52fb081b3570c81e3a) and [CSS Triggers](https://csstriggers.com/) for more information on layout-forcing operations.
2238 
2239### 7.2 Build Index Maps for Repeated Lookups
2240 
2241**Impact: LOW-MEDIUM (1M ops to 2K ops)**
2242 
2243Multiple `.find()` calls by the same key should use a Map.
2244 
2245**Incorrect (O(n) per lookup):**
2246 
2247```typescript
2248function processOrders(orders: Order[], users: User[]) {
2249 return orders.map((order) => ({
2250 ...order,
2251 user: users.find((u) => u.id === order.userId),
2252 }))
2253}
2254```
2255 
2256**Correct (O(1) per lookup):**
2257 
2258```typescript
2259function processOrders(orders: Order[], users: User[]) {
2260 const userById = new Map(users.map((u) => [u.id, u]))
2261 
2262 return orders.map((order) => ({
2263 ...order,
2264 user: userById.get(order.userId),
2265 }))
2266}
2267```
2268 
2269Build map once (O(n)), then all lookups are O(1).
2270 
2271For 1000 orders × 1000 users: 1M ops → 2K ops.
2272 
2273### 7.3 Cache Property Access in Loops
2274 
2275**Impact: LOW-MEDIUM (reduces lookups)**
2276 
2277Cache object property lookups in hot paths.
2278 
2279**Incorrect: 3 lookups × N iterations**
2280 
2281```typescript
2282for (let i = 0; i < arr.length; i++) {
2283 process(obj.config.settings.value)
2284}
2285```
2286 
2287**Correct: 1 lookup total**
2288 
2289```typescript
2290const value = obj.config.settings.value
2291const len = arr.length
2292for (let i = 0; i < len; i++) {
2293 process(value)
2294}
2295```
2296 
2297### 7.4 Cache Repeated Function Calls
2298 
2299**Impact: MEDIUM (avoid redundant computation)**
2300 
2301Use a module-level Map to cache function results when the same function is called repeatedly with the same inputs during render.
2302 
2303**Incorrect: redundant computation**
2304 
2305```typescript
2306function ProjectList({ projects }: { projects: Project[] }) {
2307 return (
2308 <div>
2309 {projects.map(project => {
2310 // slugify() called 100+ times for same project names
2311 const slug = slugify(project.name)
2312 
2313 return <ProjectCard key={project.id} slug={slug} />
2314 })}
2315 </div>
2316 )
2317}
2318```
2319 
2320**Correct: cached results**
2321 
2322```typescript
2323// Module-level cache
2324const slugifyCache = new Map<string, string>()
2325 
2326function cachedSlugify(text: string): string {
2327 if (slugifyCache.has(text)) {
2328 return slugifyCache.get(text)!
2329 }
2330 const result = slugify(text)
2331 slugifyCache.set(text, result)
2332 return result
2333}
2334 
2335function ProjectList({ projects }: { projects: Project[] }) {
2336 return (
2337 <div>
2338 {projects.map(project => {
2339 // Computed only once per unique project name
2340 const slug = cachedSlugify(project.name)
2341 
2342 return <ProjectCard key={project.id} slug={slug} />
2343 })}
2344 </div>
2345 )
2346}
2347```
2348 
2349**Simpler pattern for single-value functions:**
2350 
2351```typescript
2352let isLoggedInCache: boolean | null = null
2353 
2354function isLoggedIn(): boolean {
2355 if (isLoggedInCache !== null) {
2356 return isLoggedInCache
2357 }
2358 
2359 isLoggedInCache = document.cookie.includes("auth=")
2360 return isLoggedInCache
2361}
2362 
2363// Clear cache when auth changes
2364function onAuthChange() {
2365 isLoggedInCache = null
2366}
2367```
2368 
2369Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.
2370 
2371Reference: [https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast)
2372 
2373### 7.5 Cache Storage API Calls
2374 
2375**Impact: LOW-MEDIUM (reduces expensive I/O)**
2376 
2377`localStorage`, `sessionStorage`, and `document.cookie` are synchronous and expensive. Cache reads in memory.
2378 
2379**Incorrect: reads storage on every call**
2380 
2381```typescript
2382function getTheme() {
2383 return localStorage.getItem("theme") ?? "light"
2384}
2385// Called 10 times = 10 storage reads
2386```
2387 
2388**Correct: Map cache**
2389 
2390```typescript
2391const storageCache = new Map<string, string | null>()
2392 
2393function getLocalStorage(key: string) {
2394 if (!storageCache.has(key)) {
2395 storageCache.set(key, localStorage.getItem(key))
2396 }
2397 return storageCache.get(key)
2398}
2399 
2400function setLocalStorage(key: string, value: string) {
2401 localStorage.setItem(key, value)
2402 storageCache.set(key, value) // keep cache in sync
2403}
2404```
2405 
2406Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.
2407 
2408**Cookie caching:**
2409 
2410```typescript
2411let cookieCache: Record<string, string> | null = null
2412 
2413function getCookie(name: string) {
2414 if (!cookieCache) {
2415 cookieCache = Object.fromEntries(document.cookie.split("; ").map((c) => c.split("=")))
2416 }
2417 return cookieCache[name]
2418}
2419```
2420 
2421**Important: invalidate on external changes**
2422 
2423```typescript
2424window.addEventListener("storage", (e) => {
2425 if (e.key) storageCache.delete(e.key)
2426})
2427 
2428document.addEventListener("visibilitychange", () => {
2429 if (document.visibilityState === "visible") {
2430 storageCache.clear()
2431 }
2432})
2433```
2434 
2435If storage can change externally (another tab, server-set cookies), invalidate cache:
2436 
2437### 7.6 Combine Multiple Array Iterations
2438 
2439**Impact: LOW-MEDIUM (reduces iterations)**
2440 
2441Multiple `.filter()` or `.map()` calls iterate the array multiple times. Combine into one loop.
2442 
2443**Incorrect: 3 iterations**
2444 
2445```typescript
2446const admins = users.filter((u) => u.isAdmin)
2447const testers = users.filter((u) => u.isTester)
2448const inactive = users.filter((u) => !u.isActive)
2449```
2450 
2451**Correct: 1 iteration**
2452 
2453```typescript
2454const admins: User[] = []
2455const testers: User[] = []
2456const inactive: User[] = []
2457 
2458for (const user of users) {
2459 if (user.isAdmin) admins.push(user)
2460 if (user.isTester) testers.push(user)
2461 if (!user.isActive) inactive.push(user)
2462}
2463```
2464 
2465### 7.7 Early Length Check for Array Comparisons
2466 
2467**Impact: MEDIUM-HIGH (avoids expensive operations when lengths differ)**
2468 
2469When comparing arrays with expensive operations (sorting, deep equality, serialization), check lengths first. If lengths differ, the arrays cannot be equal.
2470 
2471In real-world applications, this optimization is especially valuable when the comparison runs in hot paths (event handlers, render loops).
2472 
2473**Incorrect: always runs expensive comparison**
2474 
2475```typescript
2476function hasChanges(current: string[], original: string[]) {
2477 // Always sorts and joins, even when lengths differ
2478 return current.sort().join() !== original.sort().join()
2479}
2480```
2481 
2482Two O(n log n) sorts run even when `current.length` is 5 and `original.length` is 100. There is also overhead of joining the arrays and comparing the strings.
2483 
2484**Correct (O(1) length check first):**
2485 
2486```typescript
2487function hasChanges(current: string[], original: string[]) {
2488 // Early return if lengths differ
2489 if (current.length !== original.length) {
2490 return true
2491 }
2492 // Only sort when lengths match
2493 const currentSorted = current.toSorted()
2494 const originalSorted = original.toSorted()
2495 for (let i = 0; i < currentSorted.length; i++) {
2496 if (currentSorted[i] !== originalSorted[i]) {
2497 return true
2498 }
2499 }
2500 return false
2501}
2502```
2503 
2504This new approach is more efficient because:
2505 
2506- It avoids the overhead of sorting and joining the arrays when lengths differ
2507 
2508- It avoids consuming memory for the joined strings (especially important for large arrays)
2509 
2510- It avoids mutating the original arrays
2511 
2512- It returns early when a difference is found
2513 
2514### 7.8 Early Return from Functions
2515 
2516**Impact: LOW-MEDIUM (avoids unnecessary computation)**
2517 
2518Return early when result is determined to skip unnecessary processing.
2519 
2520**Incorrect: processes all items even after finding answer**
2521 
2522```typescript
2523function validateUsers(users: User[]) {
2524 let hasError = false
2525 let errorMessage = ""
2526 
2527 for (const user of users) {
2528 if (!user.email) {
2529 hasError = true
2530 errorMessage = "Email required"
2531 }
2532 if (!user.name) {
2533 hasError = true
2534 errorMessage = "Name required"
2535 }
2536 // Continues checking all users even after error found
2537 }
2538 
2539 return hasError ? { valid: false, error: errorMessage } : { valid: true }
2540}
2541```
2542 
2543**Correct: returns immediately on first error**
2544 
2545```typescript
2546function validateUsers(users: User[]) {
2547 for (const user of users) {
2548 if (!user.email) {
2549 return { valid: false, error: "Email required" }
2550 }
2551 if (!user.name) {
2552 return { valid: false, error: "Name required" }
2553 }
2554 }
2555 
2556 return { valid: true }
2557}
2558```
2559 
2560### 7.9 Hoist RegExp Creation
2561 
2562**Impact: LOW-MEDIUM (avoids recreation)**
2563 
2564Don't create RegExp inside render. Hoist to module scope or memoize with `useMemo()`.
2565 
2566**Incorrect: new RegExp every render**
2567 
2568```tsx
2569function Highlighter({ text, query }: Props) {
2570 const regex = new RegExp(`(${query})`, 'gi')
2571 const parts = text.split(regex)
2572 return <>{parts.map((part, i) => ...)}</>
2573}
2574```
2575 
2576**Correct: memoize or hoist**
2577 
2578```tsx
2579const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
2580 
2581function Highlighter({ text, query }: Props) {
2582 const regex = useMemo(
2583 () => new RegExp(`(${escapeRegex(query)})`, 'gi'),
2584 [query]
2585 )
2586 const parts = text.split(regex)
2587 return <>{parts.map((part, i) => ...)}</>
2588}
2589```
2590 
2591**Warning: global regex has mutable state**
2592 
2593```typescript
2594const regex = /foo/g
2595regex.test("foo") // true, lastIndex = 3
2596regex.test("foo") // false, lastIndex = 0
2597```
2598 
2599Global regex (`/g`) has mutable `lastIndex` state:
2600 
2601### 7.10 Use Loop for Min/Max Instead of Sort
2602 
2603**Impact: LOW (O(n) instead of O(n log n))**
2604 
2605Finding the smallest or largest element only requires a single pass through the array. Sorting is wasteful and slower.
2606 
2607**Incorrect (O(n log n) - sort to find latest):**
2608 
2609```typescript
2610interface Project {
2611 id: string
2612 name: string
2613 updatedAt: number
2614}
2615 
2616function getLatestProject(projects: Project[]) {
2617 const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt)
2618 return sorted[0]
2619}
2620```
2621 
2622Sorts the entire array just to find the maximum value.
2623 
2624**Incorrect (O(n log n) - sort for oldest and newest):**
2625 
2626```typescript
2627function getOldestAndNewest(projects: Project[]) {
2628 const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt)
2629 return { oldest: sorted[0], newest: sorted[sorted.length - 1] }
2630}
2631```
2632 
2633Still sorts unnecessarily when only min/max are needed.
2634 
2635**Correct (O(n) - single loop):**
2636 
2637```typescript
2638function getLatestProject(projects: Project[]) {
2639 if (projects.length === 0) return null
2640 
2641 let latest = projects[0]
2642 
2643 for (let i = 1; i < projects.length; i++) {
2644 if (projects[i].updatedAt > latest.updatedAt) {
2645 latest = projects[i]
2646 }
2647 }
2648 
2649 return latest
2650}
2651 
2652function getOldestAndNewest(projects: Project[]) {
2653 if (projects.length === 0) return { oldest: null, newest: null }
2654 
2655 let oldest = projects[0]
2656 let newest = projects[0]
2657 
2658 for (let i = 1; i < projects.length; i++) {
2659 if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i]
2660 if (projects[i].updatedAt > newest.updatedAt) newest = projects[i]
2661 }
2662 
2663 return { oldest, newest }
2664}
2665```
2666 
2667Single pass through the array, no copying, no sorting.
2668 
2669**Alternative: Math.min/Math.max for small arrays**
2670 
2671```typescript
2672const numbers = [5, 2, 8, 1, 9]
2673const min = Math.min(...numbers)
2674const max = Math.max(...numbers)
2675```
2676 
2677This works for small arrays, but can be slower or just throw an error for very large arrays due to spread operator limitations. Maximal array length is approximately 124000 in Chrome 143 and 638000 in Safari 18; exact numbers may vary - see [the fiddle](https://jsfiddle.net/qw1jabsx/4/). Use the loop approach for reliability.
2678 
2679### 7.11 Use Set/Map for O(1) Lookups
2680 
2681**Impact: LOW-MEDIUM (O(n) to O(1))**
2682 
2683Convert arrays to Set/Map for repeated membership checks.
2684 
2685**Incorrect (O(n) per check):**
2686 
2687```typescript
2688const allowedIds = ['a', 'b', 'c', ...]
2689items.filter(item => allowedIds.includes(item.id))
2690```
2691 
2692**Correct (O(1) per check):**
2693 
2694```typescript
2695const allowedIds = new Set(['a', 'b', 'c', ...])
2696items.filter(item => allowedIds.has(item.id))
2697```
2698 
2699### 7.12 Use toSorted() Instead of sort() for Immutability
2700 
2701**Impact: MEDIUM-HIGH (prevents mutation bugs in React state)**
2702 
2703`.sort()` mutates the array in place, which can cause bugs with React state and props. Use `.toSorted()` to create a new sorted array without mutation.
2704 
2705**Incorrect: mutates original array**
2706 
2707```typescript
2708function UserList({ users }: { users: User[] }) {
2709 // Mutates the users prop array!
2710 const sorted = useMemo(
2711 () => users.sort((a, b) => a.name.localeCompare(b.name)),
2712 [users]
2713 )
2714 return <div>{sorted.map(renderUser)}</div>
2715}
2716```
2717 
2718**Correct: creates new array**
2719 
2720```typescript
2721function UserList({ users }: { users: User[] }) {
2722 // Creates new sorted array, original unchanged
2723 const sorted = useMemo(
2724 () => users.toSorted((a, b) => a.name.localeCompare(b.name)),
2725 [users]
2726 )
2727 return <div>{sorted.map(renderUser)}</div>
2728}
2729```
2730 
2731**Why this matters in React:**
2732 
27331. Props/state mutations break React's immutability model - React expects props and state to be treated as read-only
2734 
27352. Causes stale closure bugs - Mutating arrays inside closures (callbacks, effects) can lead to unexpected behavior
2736 
2737**Browser support: fallback for older browsers**
2738 
2739```typescript
2740// Fallback for older browsers
2741const sorted = [...items].sort((a, b) => a.value - b.value)
2742```
2743 
2744`.toSorted()` is available in all modern browsers (Chrome 110+, Safari 16+, Firefox 115+, Node.js 20+). For older environments, use spread operator:
2745 
2746**Other immutable array methods:**
2747 
2748- `.toSorted()` - immutable sort
2749 
2750- `.toReversed()` - immutable reverse
2751 
2752- `.toSpliced()` - immutable splice
2753 
2754- `.with()` - immutable element replacement
2755 
2756---
2757 
2758## 8. Advanced Patterns
2759 
2760**Impact: LOW**
2761 
2762Advanced patterns for specific cases that require careful implementation.
2763 
2764### 8.1 Initialize App Once, Not Per Mount
2765 
2766**Impact: LOW-MEDIUM (avoids duplicate init in development)**
2767 
2768Do not put app-wide initialization that must run once per app load inside `useEffect([])` of a component. Components can remount and effects will re-run. Use a module-level guard or top-level init in the entry module instead.
2769 
2770**Incorrect: runs twice in dev, re-runs on remount**
2771 
2772```tsx
2773function Comp() {
2774 useEffect(() => {
2775 loadFromStorage()
2776 checkAuthToken()
2777 }, [])
2778 
2779 // ...
2780}
2781```
2782 
2783**Correct: once per app load**
2784 
2785```tsx
2786let didInit = false
2787 
2788function Comp() {
2789 useEffect(() => {
2790 if (didInit) return
2791 didInit = true
2792 loadFromStorage()
2793 checkAuthToken()
2794 }, [])
2795 
2796 // ...
2797}
2798```
2799 
2800Reference: [https://react.dev/learn/you-might-not-need-an-effect#initializing-the-application](https://react.dev/learn/you-might-not-need-an-effect#initializing-the-application)
2801 
2802### 8.2 Store Event Handlers in Refs
2803 
2804**Impact: LOW (stable subscriptions)**
2805 
2806Store callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.
2807 
2808**Incorrect: re-subscribes on every render**
2809 
2810```tsx
2811function useWindowEvent(event: string, handler: (e) => void) {
2812 useEffect(() => {
2813 window.addEventListener(event, handler)
2814 return () => window.removeEventListener(event, handler)
2815 }, [event, handler])
2816}
2817```
2818 
2819**Correct: stable subscription**
2820 
2821```tsx
2822import { useEffectEvent } from "react"
2823 
2824function useWindowEvent(event: string, handler: (e) => void) {
2825 const onEvent = useEffectEvent(handler)
2826 
2827 useEffect(() => {
2828 window.addEventListener(event, onEvent)
2829 return () => window.removeEventListener(event, onEvent)
2830 }, [event])
2831}
2832```
2833 
2834**Alternative: use `useEffectEvent` if you're on latest React:**
2835 
2836`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.
2837 
2838### 8.3 useEffectEvent for Stable Callback Refs
2839 
2840**Impact: LOW (prevents effect re-runs)**
2841 
2842Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.
2843 
2844**Incorrect: effect re-runs on every callback change**
2845 
2846```tsx
2847function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
2848 const [query, setQuery] = useState("")
2849 
2850 useEffect(() => {
2851 const timeout = setTimeout(() => onSearch(query), 300)
2852 return () => clearTimeout(timeout)
2853 }, [query, onSearch])
2854}
2855```
2856 
2857**Correct: using React's useEffectEvent**
2858 
2859```tsx
2860import { useEffectEvent } from "react"
2861 
2862function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
2863 const [query, setQuery] = useState("")
2864 const onSearchEvent = useEffectEvent(onSearch)
2865 
2866 useEffect(() => {
2867 const timeout = setTimeout(() => onSearchEvent(query), 300)
2868 return () => clearTimeout(timeout)
2869 }, [query])
2870}
2871```
2872 
2873---
2874 
2875## References
2876 
28771. [https://react.dev](https://react.dev)
28782. [https://nextjs.org](https://nextjs.org)
28793. [https://swr.vercel.app](https://swr.vercel.app)
28804. [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
28815. [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)
28826. [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)
28837. [https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast)
2884 
zebbern/claude-code-guide · skills/composition-patterns/AGENTS.md
@@ +1 @@
1# React Composition Patterns
2 
3**Version 1.0.0**
4Engineering
5January 2026
6 
7> **Note:**
8> This document is mainly for agents and LLMs to follow when maintaining,
9> generating, or refactoring React codebases using composition. Humans
10> may also find it useful, but guidance here is optimized for automation
11> and consistency by AI-assisted workflows.
12 
13---
14 
15## Abstract
16 
17Composition patterns for building flexible, maintainable React components. Avoid boolean prop proliferation by using compound components, lifting state, and composing internals. These patterns make codebases easier for both humans and AI agents to work with as they scale.
18 
19---
20 
21## Table of Contents
22 
231. [Component Architecture](#1-component-architecture) — **HIGH**
24 - 1.1 [Avoid Boolean Prop Proliferation](#11-avoid-boolean-prop-proliferation)
25 - 1.2 [Use Compound Components](#12-use-compound-components)
262. [State Management](#2-state-management) — **MEDIUM**
27 - 2.1 [Decouple State Management from UI](#21-decouple-state-management-from-ui)
28 - 2.2 [Define Generic Context Interfaces for Dependency Injection](#22-define-generic-context-interfaces-for-dependency-injection)
29 - 2.3 [Lift State into Provider Components](#23-lift-state-into-provider-components)
303. [Implementation Patterns](#3-implementation-patterns) — **MEDIUM**
31 - 3.1 [Create Explicit Component Variants](#31-create-explicit-component-variants)
32 - 3.2 [Prefer Composing Children Over Render Props](#32-prefer-composing-children-over-render-props)
334. [React 19 APIs](#4-react-19-apis) — **MEDIUM**
34 - 4.1 [React 19 API Changes](#41-react-19-api-changes)
35 
36---
37 
38## 1. Component Architecture
39 
40**Impact: HIGH**
41 
42Fundamental patterns for structuring components to avoid prop
43proliferation and enable flexible composition.
44 
45### 1.1 Avoid Boolean Prop Proliferation
46 
47**Impact: CRITICAL (prevents unmaintainable component variants)**
48 
49Don't add boolean props like `isThread`, `isEditing`, `isDMThread` to customize
50 
51component behavior. Each boolean doubles possible states and creates
52 
53unmaintainable conditional logic. Use composition instead.
54 
55**Incorrect: boolean props create exponential complexity**
56 
57```tsx
58function Composer({
59 onSubmit,
60 isThread,
61 channelId,
62 isDMThread,
63 dmId,
64 isEditing,
65 isForwarding,
66}: Props) {
67 return (
68 <form>
69 <Header />
70 <Input />
71 {isDMThread ? (
72 <AlsoSendToDMField id={dmId} />
73 ) : isThread ? (
74 <AlsoSendToChannelField id={channelId} />
75 ) : null}
76 {isEditing ? <EditActions /> : isForwarding ? <ForwardActions /> : <DefaultActions />}
77 <Footer onSubmit={onSubmit} />
78 </form>
79 )
80}
81```
82 
83**Correct: composition eliminates conditionals**
84 
85```tsx
86// Channel composer
87function ChannelComposer() {
88 return (
89 <Composer.Frame>
90 <Composer.Header />
91 <Composer.Input />
92 <Composer.Footer>
93 <Composer.Attachments />
94 <Composer.Formatting />
95 <Composer.Emojis />
96 <Composer.Submit />
97 </Composer.Footer>
98 </Composer.Frame>
99 )
100}
101 
102// Thread composer - adds "also send to channel" field
103function ThreadComposer({ channelId }: { channelId: string }) {
104 return (
105 <Composer.Frame>
106 <Composer.Header />
107 <Composer.Input />
108 <AlsoSendToChannelField id={channelId} />
109 <Composer.Footer>
110 <Composer.Formatting />
111 <Composer.Emojis />
112 <Composer.Submit />
113 </Composer.Footer>
114 </Composer.Frame>
115 )
116}
117 
118// Edit composer - different footer actions
119function EditComposer() {
120 return (
121 <Composer.Frame>
122 <Composer.Input />
123 <Composer.Footer>
124 <Composer.Formatting />
125 <Composer.Emojis />
126 <Composer.CancelEdit />
127 <Composer.SaveEdit />
128 </Composer.Footer>
129 </Composer.Frame>
130 )
131}
132```
133 
134Each variant is explicit about what it renders. We can share internals without
135 
136sharing a single monolithic parent.
137 
138### 1.2 Use Compound Components
139 
140**Impact: HIGH (enables flexible composition without prop drilling)**
141 
142Structure complex components as compound components with a shared context. Each
143 
144subcomponent accesses shared state via context, not props. Consumers compose the
145 
146pieces they need.
147 
148**Incorrect: monolithic component with render props**
149 
150```tsx
151function Composer({
152 renderHeader,
153 renderFooter,
154 renderActions,
155 showAttachments,
156 showFormatting,
157 showEmojis,
158}: Props) {
159 return (
160 <form>
161 {renderHeader?.()}
162 <Input />
163 {showAttachments && <Attachments />}
164 {renderFooter ? (
165 renderFooter()
166 ) : (
167 <Footer>
168 {showFormatting && <Formatting />}
169 {showEmojis && <Emojis />}
170 {renderActions?.()}
171 </Footer>
172 )}
173 </form>
174 )
175}
176```
177 
178**Correct: compound components with shared context**
179 
180```tsx
181const ComposerContext = createContext<ComposerContextValue | null>(null)
182 
183function ComposerProvider({ children, state, actions, meta }: ProviderProps) {
184 return <ComposerContext value={{ state, actions, meta }}>{children}</ComposerContext>
185}
186 
187function ComposerFrame({ children }: { children: React.ReactNode }) {
188 return <form>{children}</form>
189}
190 
191function ComposerInput() {
192 const {
193 state,
194 actions: { update },
195 meta: { inputRef },
196 } = use(ComposerContext)
197 return (
198 <TextInput
199 ref={inputRef}
200 value={state.input}
201 onChangeText={(text) => update((s) => ({ ...s, input: text }))}
202 />
203 )
204}
205 
206function ComposerSubmit() {
207 const {
208 actions: { submit },
209 } = use(ComposerContext)
210 return <Button onPress={submit}>Send</Button>
211}
212 
213// Export as compound component
214const Composer = {
215 Provider: ComposerProvider,
216 Frame: ComposerFrame,
217 Input: ComposerInput,
218 Submit: ComposerSubmit,
219 Header: ComposerHeader,
220 Footer: ComposerFooter,
221 Attachments: ComposerAttachments,
222 Formatting: ComposerFormatting,
223 Emojis: ComposerEmojis,
224}
225```
226 
227**Usage:**
228 
229```tsx
230<Composer.Provider state={state} actions={actions} meta={meta}>
231 <Composer.Frame>
232 <Composer.Header />
233 <Composer.Input />
234 <Composer.Footer>
235 <Composer.Formatting />
236 <Composer.Submit />
237 </Composer.Footer>
238 </Composer.Frame>
239</Composer.Provider>
240```
241 
242Consumers explicitly compose exactly what they need. No hidden conditionals. And the state, actions and meta are dependency-injected by a parent provider, allowing multiple usages of the same component structure.
243 
244---
245 
246## 2. State Management
247 
248**Impact: MEDIUM**
249 
250Patterns for lifting state and managing shared context across
251composed components.
252 
253### 2.1 Decouple State Management from UI
254 
255**Impact: MEDIUM (enables swapping state implementations without changing UI)**
256 
257The provider component should be the only place that knows how state is managed.
258 
259UI components consume the context interface—they don't know if state comes from
260 
261useState, Zustand, or a server sync.
262 
263**Incorrect: UI coupled to state implementation**
264 
265```tsx
266function ChannelComposer({ channelId }: { channelId: string }) {
267 // UI component knows about global state implementation
268 const state = useGlobalChannelState(channelId)
269 const { submit, updateInput } = useChannelSync(channelId)
270 
271 return (
272 <Composer.Frame>
273 <Composer.Input value={state.input} onChange={(text) => sync.updateInput(text)} />
274 <Composer.Submit onPress={() => sync.submit()} />
275 </Composer.Frame>
276 )
277}
278```
279 
280**Correct: state management isolated in provider**
281 
282```tsx
283// Provider handles all state management details
284function ChannelProvider({
285 channelId,
286 children,
287}: {
288 channelId: string
289 children: React.ReactNode
290}) {
291 const { state, update, submit } = useGlobalChannel(channelId)
292 const inputRef = useRef(null)
293 
294 return (
295 <Composer.Provider state={state} actions={{ update, submit }} meta={{ inputRef }}>
296 {children}
297 </Composer.Provider>
298 )
299}
300 
301// UI component only knows about the context interface
302function ChannelComposer() {
303 return (
304 <Composer.Frame>
305 <Composer.Header />
306 <Composer.Input />
307 <Composer.Footer>
308 <Composer.Submit />
309 </Composer.Footer>
310 </Composer.Frame>
311 )
312}
313 
314// Usage
315function Channel({ channelId }: { channelId: string }) {
316 return (
317 <ChannelProvider channelId={channelId}>
318 <ChannelComposer />
319 </ChannelProvider>
320 )
321}
322```
323 
324**Different providers, same UI:**
325 
326```tsx
327// Local state for ephemeral forms
328function ForwardMessageProvider({ children }) {
329 const [state, setState] = useState(initialState)
330 const forwardMessage = useForwardMessage()
331 
332 return (
333 <Composer.Provider state={state} actions={{ update: setState, submit: forwardMessage }}>
334 {children}
335 </Composer.Provider>
336 )
337}
338 
339// Global synced state for channels
340function ChannelProvider({ channelId, children }) {
341 const { state, update, submit } = useGlobalChannel(channelId)
342 
343 return (
344 <Composer.Provider state={state} actions={{ update, submit }}>
345 {children}
346 </Composer.Provider>
347 )
348}
349```
350 
351The same `Composer.Input` component works with both providers because it only
352 
353depends on the context interface, not the implementation.
354 
355### 2.2 Define Generic Context Interfaces for Dependency Injection
356 
357**Impact: HIGH (enables dependency-injectable state across use-cases)**
358 
359Define a **generic interface** for your component context with three parts:
360 
361`state`, `actions`, and `meta`. This interface is a contract that any provider
362 
363can implement—enabling the same UI components to work with completely different
364 
365state implementations.
366 
367**Core principle:** Lift state, compose internals, make state
368 
369dependency-injectable.
370 
371**Incorrect: UI coupled to specific state implementation**
372 
373```tsx
374function ComposerInput() {
375 // Tightly coupled to a specific hook
376 const { input, setInput } = useChannelComposerState()
377 return <TextInput value={input} onChangeText={setInput} />
378}
379```
380 
381**Correct: generic interface enables dependency injection**
382 
383```tsx
384// Define a GENERIC interface that any provider can implement
385interface ComposerState {
386 input: string
387 attachments: Attachment[]
388 isSubmitting: boolean
389}
390 
391interface ComposerActions {
392 update: (updater: (state: ComposerState) => ComposerState) => void
393 submit: () => void
394}
395 
396interface ComposerMeta {
397 inputRef: React.RefObject<TextInput>
398}
399 
400interface ComposerContextValue {
401 state: ComposerState
402 actions: ComposerActions
403 meta: ComposerMeta
404}
405 
406const ComposerContext = createContext<ComposerContextValue | null>(null)
407```
408 
409**UI components consume the interface, not the implementation:**
410 
411```tsx
412function ComposerInput() {
413 const {
414 state,
415 actions: { update },
416 meta,
417 } = use(ComposerContext)
418 
419 // This component works with ANY provider that implements the interface
420 return (
421 <TextInput
422 ref={meta.inputRef}
423 value={state.input}
424 onChangeText={(text) => update((s) => ({ ...s, input: text }))}
425 />
426 )
427}
428```
429 
430**Different providers implement the same interface:**
431 
432```tsx
433// Provider A: Local state for ephemeral forms
434function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
435 const [state, setState] = useState(initialState)
436 const inputRef = useRef(null)
437 const submit = useForwardMessage()
438 
439 return (
440 <ComposerContext
441 value={{
442 state,
443 actions: { update: setState, submit },
444 meta: { inputRef },
445 }}
446 >
447 {children}
448 </ComposerContext>
449 )
450}
451 
452// Provider B: Global synced state for channels
453function ChannelProvider({ channelId, children }: Props) {
454 const { state, update, submit } = useGlobalChannel(channelId)
455 const inputRef = useRef(null)
456 
457 return (
458 <ComposerContext
459 value={{
460 state,
461 actions: { update, submit },
462 meta: { inputRef },
463 }}
464 >
465 {children}
466 </ComposerContext>
467 )
468}
469```
470 
471**The same composed UI works with both:**
472 
473```tsx
474// Works with ForwardMessageProvider (local state)
475<ForwardMessageProvider>
476 <Composer.Frame>
477 <Composer.Input />
478 <Composer.Submit />
479 </Composer.Frame>
480</ForwardMessageProvider>
481 
482// Works with ChannelProvider (global synced state)
483<ChannelProvider channelId="abc">
484 <Composer.Frame>
485 <Composer.Input />
486 <Composer.Submit />
487 </Composer.Frame>
488</ChannelProvider>
489```
490 
491**Custom UI outside the component can access state and actions:**
492 
493```tsx
494function ForwardMessageDialog() {
495 return (
496 <ForwardMessageProvider>
497 <Dialog>
498 {/* The composer UI */}
499 <Composer.Frame>
500 <Composer.Input placeholder="Add a message, if you'd like." />
501 <Composer.Footer>
502 <Composer.Formatting />
503 <Composer.Emojis />
504 </Composer.Footer>
505 </Composer.Frame>
506 
507 {/* Custom UI OUTSIDE the composer, but INSIDE the provider */}
508 <MessagePreview />
509 
510 {/* Actions at the bottom of the dialog */}
511 <DialogActions>
512 <CancelButton />
513 <ForwardButton />
514 </DialogActions>
515 </Dialog>
516 </ForwardMessageProvider>
517 )
518}
519 
520// This button lives OUTSIDE Composer.Frame but can still submit based on its context!
521function ForwardButton() {
522 const {
523 actions: { submit },
524 } = use(ComposerContext)
525 return <Button onPress={submit}>Forward</Button>
526}
527 
528// This preview lives OUTSIDE Composer.Frame but can read composer's state!
529function MessagePreview() {
530 const { state } = use(ComposerContext)
531 return <Preview message={state.input} attachments={state.attachments} />
532}
533```
534 
535The provider boundary is what matters—not the visual nesting. Components that
536 
537need shared state don't have to be inside the `Composer.Frame`. They just need
538 
539to be within the provider.
540 
541The `ForwardButton` and `MessagePreview` are not visually inside the composer
542 
543box, but they can still access its state and actions. This is the power of
544 
545lifting state into providers.
546 
547The UI is reusable bits you compose together. The state is dependency-injected
548 
549by the provider. Swap the provider, keep the UI.
550 
551### 2.3 Lift State into Provider Components
552 
553**Impact: HIGH (enables state sharing outside component boundaries)**
554 
555Move state management into dedicated provider components. This allows sibling
556 
557components outside the main UI to access and modify state without prop drilling
558 
559or awkward refs.
560 
561**Incorrect: state trapped inside component**
562 
563```tsx
564function ForwardMessageComposer() {
565 const [state, setState] = useState(initialState)
566 const forwardMessage = useForwardMessage()
567 
568 return (
569 <Composer.Frame>
570 <Composer.Input />
571 <Composer.Footer />
572 </Composer.Frame>
573 )
574}
575 
576// Problem: How does this button access composer state?
577function ForwardMessageDialog() {
578 return (
579 <Dialog>
580 <ForwardMessageComposer />
581 <MessagePreview /> {/* Needs composer state */}
582 <DialogActions>
583 <CancelButton />
584 <ForwardButton /> {/* Needs to call submit */}
585 </DialogActions>
586 </Dialog>
587 )
588}
589```
590 
591**Incorrect: useEffect to sync state up**
592 
593```tsx
594function ForwardMessageDialog() {
595 const [input, setInput] = useState("")
596 return (
597 <Dialog>
598 <ForwardMessageComposer onInputChange={setInput} />
599 <MessagePreview input={input} />
600 </Dialog>
601 )
602}
603 
604function ForwardMessageComposer({ onInputChange }) {
605 const [state, setState] = useState(initialState)
606 useEffect(() => {
607 onInputChange(state.input) // Sync on every change 😬
608 }, [state.input])
609}
610```
611 
612**Incorrect: reading state from ref on submit**
613 
614```tsx
615function ForwardMessageDialog() {
616 const stateRef = useRef(null)
617 return (
618 <Dialog>
619 <ForwardMessageComposer stateRef={stateRef} />
620 <ForwardButton onPress={() => submit(stateRef.current)} />
621 </Dialog>
622 )
623}
624```
625 
626**Correct: state lifted to provider**
627 
628```tsx
629function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
630 const [state, setState] = useState(initialState)
631 const forwardMessage = useForwardMessage()
632 const inputRef = useRef(null)
633 
634 return (
635 <Composer.Provider
636 state={state}
637 actions={{ update: setState, submit: forwardMessage }}
638 meta={{ inputRef }}
639 >
640 {children}
641 </Composer.Provider>
642 )
643}
644 
645function ForwardMessageDialog() {
646 return (
647 <ForwardMessageProvider>
648 <Dialog>
649 <ForwardMessageComposer />
650 <MessagePreview /> {/* Custom components can access state and actions */}
651 <DialogActions>
652 <CancelButton />
653 <ForwardButton /> {/* Custom components can access state and actions */}
654 </DialogActions>
655 </Dialog>
656 </ForwardMessageProvider>
657 )
658}
659 
660function ForwardButton() {
661 const { actions } = use(Composer.Context)
662 return <Button onPress={actions.submit}>Forward</Button>
663}
664```
665 
666The ForwardButton lives outside the Composer.Frame but still has access to the
667 
668submit action because it's within the provider. Even though it's a one-off
669 
670component, it can still access the composer's state and actions from outside the
671 
672UI itself.
673 
674**Key insight:** Components that need shared state don't have to be visually
675 
676nested inside each other—they just need to be within the same provider.
677 
678---
679 
680## 3. Implementation Patterns
681 
682**Impact: MEDIUM**
683 
684Specific techniques for implementing compound components and
685context providers.
686 
687### 3.1 Create Explicit Component Variants
688 
689**Impact: MEDIUM (self-documenting code, no hidden conditionals)**
690 
691Instead of one component with many boolean props, create explicit variant
692 
693components. Each variant composes the pieces it needs. The code documents
694 
695itself.
696 
697**Incorrect: one component, many modes**
698 
699```tsx
700// What does this component actually render?
701<Composer isThread isEditing={false} channelId="abc" showAttachments showFormatting={false} />
702```
703 
704**Correct: explicit variants**
705 
706```tsx
707// Immediately clear what this renders
708<ThreadComposer channelId="abc" />
709 
710// Or
711<EditMessageComposer messageId="xyz" />
712 
713// Or
714<ForwardMessageComposer messageId="123" />
715```
716 
717Each implementation is unique, explicit and self-contained. Yet they can each
718 
719use shared parts.
720 
721**Implementation:**
722 
723```tsx
724function ThreadComposer({ channelId }: { channelId: string }) {
725 return (
726 <ThreadProvider channelId={channelId}>
727 <Composer.Frame>
728 <Composer.Input />
729 <AlsoSendToChannelField channelId={channelId} />
730 <Composer.Footer>
731 <Composer.Formatting />
732 <Composer.Emojis />
733 <Composer.Submit />
734 </Composer.Footer>
735 </Composer.Frame>
736 </ThreadProvider>
737 )
738}
739 
740function EditMessageComposer({ messageId }: { messageId: string }) {
741 return (
742 <EditMessageProvider messageId={messageId}>
743 <Composer.Frame>
744 <Composer.Input />
745 <Composer.Footer>
746 <Composer.Formatting />
747 <Composer.Emojis />
748 <Composer.CancelEdit />
749 <Composer.SaveEdit />
750 </Composer.Footer>
751 </Composer.Frame>
752 </EditMessageProvider>
753 )
754}
755 
756function ForwardMessageComposer({ messageId }: { messageId: string }) {
757 return (
758 <ForwardMessageProvider messageId={messageId}>
759 <Composer.Frame>
760 <Composer.Input placeholder="Add a message, if you'd like." />
761 <Composer.Footer>
762 <Composer.Formatting />
763 <Composer.Emojis />
764 <Composer.Mentions />
765 </Composer.Footer>
766 </Composer.Frame>
767 </ForwardMessageProvider>
768 )
769}
770```
771 
772Each variant is explicit about:
773 
774- What provider/state it uses
775 
776- What UI elements it includes
777 
778- What actions are available
779 
780No boolean prop combinations to reason about. No impossible states.
781 
782### 3.2 Prefer Composing Children Over Render Props
783 
784**Impact: MEDIUM (cleaner composition, better readability)**
785 
786Use `children` for composition instead of `renderX` props. Children are more
787 
788readable, compose naturally, and don't require understanding callback
789 
790signatures.
791 
792**Incorrect: render props**
793 
794```tsx
795function Composer({
796 renderHeader,
797 renderFooter,
798 renderActions,
799}: {
800 renderHeader?: () => React.ReactNode
801 renderFooter?: () => React.ReactNode
802 renderActions?: () => React.ReactNode
803}) {
804 return (
805 <form>
806 {renderHeader?.()}
807 <Input />
808 {renderFooter ? renderFooter() : <DefaultFooter />}
809 {renderActions?.()}
810 </form>
811 )
812}
813 
814// Usage is awkward and inflexible
815return (
816 <Composer
817 renderHeader={() => <CustomHeader />}
818 renderFooter={() => (
819 <>
820 <Formatting />
821 <Emojis />
822 </>
823 )}
824 renderActions={() => <SubmitButton />}
825 />
826)
827```
828 
829**Correct: compound components with children**
830 
831```tsx
832function ComposerFrame({ children }: { children: React.ReactNode }) {
833 return <form>{children}</form>
834}
835 
836function ComposerFooter({ children }: { children: React.ReactNode }) {
837 return <footer className="flex">{children}</footer>
838}
839 
840// Usage is flexible
841return (
842 <Composer.Frame>
843 <CustomHeader />
844 <Composer.Input />
845 <Composer.Footer>
846 <Composer.Formatting />
847 <Composer.Emojis />
848 <SubmitButton />
849 </Composer.Footer>
850 </Composer.Frame>
851)
852```
853 
854**When render props are appropriate:**
855 
856```tsx
857// Render props work well when you need to pass data back
858<List data={items} renderItem={({ item, index }) => <Item item={item} index={index} />} />
859```
860 
861Use render props when the parent needs to provide data or state to the child.
862 
863Use children when composing static structure.
864 
865---
866 
867## 4. React 19 APIs
868 
869**Impact: MEDIUM**
870 
871React 19+ only. Don't use `forwardRef`; use `use()` instead of `useContext()`.
872 
873### 4.1 React 19 API Changes
874 
875**Impact: MEDIUM (cleaner component definitions and context usage)**
876 
877> **⚠️ React 19+ only.** Skip this if you're on React 18 or earlier.
878 
879In React 19, `ref` is now a regular prop (no `forwardRef` wrapper needed), and `use()` replaces `useContext()`.
880 
881**Incorrect: forwardRef in React 19**
882 
883```tsx
884const ComposerInput = forwardRef<TextInput, Props>((props, ref) => {
885 return <TextInput ref={ref} {...props} />
886})
887```
888 
889**Correct: ref as a regular prop**
890 
891```tsx
892function ComposerInput({ ref, ...props }: Props & { ref?: React.Ref<TextInput> }) {
893 return <TextInput ref={ref} {...props} />
894}
895```
896 
897**Incorrect: useContext in React 19**
898 
899```tsx
900const value = useContext(MyContext)
901```
902 
903**Correct: use instead of useContext**
904 
905```tsx
906const value = use(MyContext)
907```
908 
909`use()` can also be called conditionally, unlike `useContext()`.
910 
911---
912 
913## References
914 
9151. [https://react.dev](https://react.dev)
9162. [https://react.dev/learn/passing-data-deeply-with-context](https://react.dev/learn/passing-data-deeply-with-context)
9173. [https://react.dev/reference/react/use](https://react.dev/reference/react/use)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
918 
@@ −1 +1 @@
1−# React Best Practices
2− 
3−**Version 1.0.0**
4−Vercel Engineering
5−January 2026
6− 
7−> **Note:**
8−> This document is mainly for agents and LLMs to follow when maintaining,
9−> generating, or refactoring React and Next.js codebases. Humans
10−> may also find it useful, but guidance here is optimized for automation
11−> and consistency by AI-assisted workflows.
12− 
13−---
14− 
15−## Abstract
16− 
17−Comprehensive performance optimization guide for React and Next.js applications, designed for AI agents and LLMs. Contains 40+ rules across 8 categories, prioritized by impact from critical (eliminating waterfalls, reducing bundle size) to incremental (advanced patterns). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation.
18− 
19−---
20− 
21−## Table of Contents
22− 
23−1. [Eliminating Waterfalls](#1-eliminating-waterfalls) — **CRITICAL**
24− - 1.1 [Defer Await Until Needed](#11-defer-await-until-needed)
25− - 1.2 [Dependency-Based Parallelization](#12-dependency-based-parallelization)
26− - 1.3 [Prevent Waterfall Chains in API Routes](#13-prevent-waterfall-chains-in-api-routes)
27− - 1.4 [Promise.all() for Independent Operations](#14-promiseall-for-independent-operations)
28− - 1.5 [Strategic Suspense Boundaries](#15-strategic-suspense-boundaries)
29−2. [Bundle Size Optimization](#2-bundle-size-optimization) — **CRITICAL**
30− - 2.1 [Avoid Barrel File Imports](#21-avoid-barrel-file-imports)
31− - 2.2 [Conditional Module Loading](#22-conditional-module-loading)
32− - 2.3 [Defer Non-Critical Third-Party Libraries](#23-defer-non-critical-third-party-libraries)
33− - 2.4 [Dynamic Imports for Heavy Components](#24-dynamic-imports-for-heavy-components)
34− - 2.5 [Preload Based on User Intent](#25-preload-based-on-user-intent)
35−3. [Server-Side Performance](#3-server-side-performance) — **HIGH**
36− - 3.1 [Authenticate Server Actions Like API Routes](#31-authenticate-server-actions-like-api-routes)
37− - 3.2 [Avoid Duplicate Serialization in RSC Props](#32-avoid-duplicate-serialization-in-rsc-props)
38− - 3.3 [Cross-Request LRU Caching](#33-cross-request-lru-caching)
39− - 3.4 [Minimize Serialization at RSC Boundaries](#34-minimize-serialization-at-rsc-boundaries)
40− - 3.5 [Parallel Data Fetching with Component Composition](#35-parallel-data-fetching-with-component-composition)
41− - 3.6 [Per-Request Deduplication with React.cache()](#36-per-request-deduplication-with-reactcache)
42− - 3.7 [Use after() for Non-Blocking Operations](#37-use-after-for-non-blocking-operations)
43−4. [Client-Side Data Fetching](#4-client-side-data-fetching) — **MEDIUM-HIGH**
44− - 4.1 [Deduplicate Global Event Listeners](#41-deduplicate-global-event-listeners)
45− - 4.2 [Use Passive Event Listeners for Scrolling Performance](#42-use-passive-event-listeners-for-scrolling-performance)
46− - 4.3 [Use SWR for Automatic Deduplication](#43-use-swr-for-automatic-deduplication)
47− - 4.4 [Version and Minimize localStorage Data](#44-version-and-minimize-localstorage-data)
48−5. [Re-render Optimization](#5-re-render-optimization) — **MEDIUM**
49− - 5.1 [Calculate Derived State During Rendering](#51-calculate-derived-state-during-rendering)
50− - 5.2 [Defer State Reads to Usage Point](#52-defer-state-reads-to-usage-point)
51− - 5.3 [Do not wrap a simple expression with a primitive result type in useMemo](#53-do-not-wrap-a-simple-expression-with-a-primitive-result-type-in-usememo)
52− - 5.4 [Extract Default Non-primitive Parameter Value from Memoized Component to Constant](#54-extract-default-non-primitive-parameter-value-from-memoized-component-to-constant)
53− - 5.5 [Extract to Memoized Components](#55-extract-to-memoized-components)
54− - 5.6 [Narrow Effect Dependencies](#56-narrow-effect-dependencies)
55− - 5.7 [Put Interaction Logic in Event Handlers](#57-put-interaction-logic-in-event-handlers)
56− - 5.8 [Subscribe to Derived State](#58-subscribe-to-derived-state)
57− - 5.9 [Use Functional setState Updates](#59-use-functional-setstate-updates)
58− - 5.10 [Use Lazy State Initialization](#510-use-lazy-state-initialization)
59− - 5.11 [Use Transitions for Non-Urgent Updates](#511-use-transitions-for-non-urgent-updates)
60− - 5.12 [Use useRef for Transient Values](#512-use-useref-for-transient-values)
61−6. [Rendering Performance](#6-rendering-performance) — **MEDIUM**
62− - 6.1 [Animate SVG Wrapper Instead of SVG Element](#61-animate-svg-wrapper-instead-of-svg-element)
63− - 6.2 [CSS content-visibility for Long Lists](#62-css-content-visibility-for-long-lists)
64− - 6.3 [Hoist Static JSX Elements](#63-hoist-static-jsx-elements)
65− - 6.4 [Optimize SVG Precision](#64-optimize-svg-precision)
66− - 6.5 [Prevent Hydration Mismatch Without Flickering](#65-prevent-hydration-mismatch-without-flickering)
67− - 6.6 [Suppress Expected Hydration Mismatches](#66-suppress-expected-hydration-mismatches)
68− - 6.7 [Use Activity Component for Show/Hide](#67-use-activity-component-for-showhide)
69− - 6.8 [Use Explicit Conditional Rendering](#68-use-explicit-conditional-rendering)
70− - 6.9 [Use useTransition Over Manual Loading States](#69-use-usetransition-over-manual-loading-states)
71−7. [JavaScript Performance](#7-javascript-performance) — **LOW-MEDIUM**
72− - 7.1 [Avoid Layout Thrashing](#71-avoid-layout-thrashing)
73− - 7.2 [Build Index Maps for Repeated Lookups](#72-build-index-maps-for-repeated-lookups)
74− - 7.3 [Cache Property Access in Loops](#73-cache-property-access-in-loops)
75− - 7.4 [Cache Repeated Function Calls](#74-cache-repeated-function-calls)
76− - 7.5 [Cache Storage API Calls](#75-cache-storage-api-calls)
77− - 7.6 [Combine Multiple Array Iterations](#76-combine-multiple-array-iterations)
78− - 7.7 [Early Length Check for Array Comparisons](#77-early-length-check-for-array-comparisons)
79− - 7.8 [Early Return from Functions](#78-early-return-from-functions)
80− - 7.9 [Hoist RegExp Creation](#79-hoist-regexp-creation)
81− - 7.10 [Use Loop for Min/Max Instead of Sort](#710-use-loop-for-minmax-instead-of-sort)
82− - 7.11 [Use Set/Map for O(1) Lookups](#711-use-setmap-for-o1-lookups)
83− - 7.12 [Use toSorted() Instead of sort() for Immutability](#712-use-tosorted-instead-of-sort-for-immutability)
84−8. [Advanced Patterns](#8-advanced-patterns) — **LOW**
85− - 8.1 [Initialize App Once, Not Per Mount](#81-initialize-app-once-not-per-mount)
86− - 8.2 [Store Event Handlers in Refs](#82-store-event-handlers-in-refs)
87− - 8.3 [useEffectEvent for Stable Callback Refs](#83-useeffectevent-for-stable-callback-refs)
88− 
89−---
90− 
91−## 1. Eliminating Waterfalls
92− 
93−**Impact: CRITICAL**
94− 
95−Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains.
96− 
97−### 1.1 Defer Await Until Needed
98− 
99−**Impact: HIGH (avoids blocking unused code paths)**
100− 
101−Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them.
102− 
103−**Incorrect: blocks both branches**
104− 
105−```typescript
106−async function handleRequest(userId: string, skipProcessing: boolean) {
107− const userData = await fetchUserData(userId)
108− 
109− if (skipProcessing) {
110− // Returns immediately but still waited for userData
111− return { skipped: true }
112− }
113− 
114− // Only this branch uses userData
115− return processUserData(userData)
116−}
117−```
118− 
119−**Correct: only blocks when needed**
120− 
121−```typescript
122−async function handleRequest(userId: string, skipProcessing: boolean) {
123− if (skipProcessing) {
124− // Returns immediately without waiting
125− return { skipped: true }
126− }
127− 
128− // Fetch only when needed
129− const userData = await fetchUserData(userId)
130− return processUserData(userData)
131−}
132−```
133− 
134−**Another example: early return optimization**
135− 
136−```typescript
137−// Incorrect: always fetches permissions
138−async function updateResource(resourceId: string, userId: string) {
139− const permissions = await fetchPermissions(userId)
140− const resource = await getResource(resourceId)
141− 
142− if (!resource) {
143− return { error: "Not found" }
144− }
145− 
146− if (!permissions.canEdit) {
147− return { error: "Forbidden" }
148− }
149− 
150− return await updateResourceData(resource, permissions)
151−}
152− 
153−// Correct: fetches only when needed
154−async function updateResource(resourceId: string, userId: string) {
155− const resource = await getResource(resourceId)
156− 
157− if (!resource) {
158− return { error: "Not found" }
159− }
160− 
161− const permissions = await fetchPermissions(userId)
162− 
163− if (!permissions.canEdit) {
164− return { error: "Forbidden" }
165− }
166− 
167− return await updateResourceData(resource, permissions)
168−}
169−```
170− 
171−This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive.
172− 
173−### 1.2 Dependency-Based Parallelization
174− 
175−**Impact: CRITICAL (2-10× improvement)**
176− 
177−For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment.
178− 
179−**Incorrect: profile waits for config unnecessarily**
180− 
181−```typescript
182−const [user, config] = await Promise.all([fetchUser(), fetchConfig()])
183−const profile = await fetchProfile(user.id)
184−```
185− 
186−**Correct: config and profile run in parallel**
187− 
188−```typescript
189−import { all } from "better-all"
190− 
191−const { user, config, profile } = await all({
192− async user() {
193− return fetchUser()
194− },
195− async config() {
196− return fetchConfig()
197− },
198− async profile() {
199− return fetchProfile((await this.$.user).id)
200− },
201−})
202−```
203− 
204−**Alternative without extra dependencies:**
205− 
206−```typescript
207−const userPromise = fetchUser()
208−const profilePromise = userPromise.then((user) => fetchProfile(user.id))
209− 
210−const [user, config, profile] = await Promise.all([userPromise, fetchConfig(), profilePromise])
211−```
212− 
213−We can also create all the promises first, and do `Promise.all()` at the end.
214− 
215−Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
216− 
217−### 1.3 Prevent Waterfall Chains in API Routes
218− 
219−**Impact: CRITICAL (2-10× improvement)**
220− 
221−In API routes and Server Actions, start independent operations immediately, even if you don't await them yet.
222− 
223−**Incorrect: config waits for auth, data waits for both**
224− 
225−```typescript
226−export async function GET(request: Request) {
227− const session = await auth()
228− const config = await fetchConfig()
229− const data = await fetchData(session.user.id)
230− return Response.json({ data, config })
231−}
232−```
233− 
234−**Correct: auth and config start immediately**
235− 
236−```typescript
237−export async function GET(request: Request) {
238− const sessionPromise = auth()
239− const configPromise = fetchConfig()
240− const session = await sessionPromise
241− const [config, data] = await Promise.all([configPromise, fetchData(session.user.id)])
242− return Response.json({ data, config })
243−}
244−```
245− 
246−For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization).
247− 
248−### 1.4 Promise.all() for Independent Operations
249− 
250−**Impact: CRITICAL (2-10× improvement)**
251− 
252−When async operations have no interdependencies, execute them concurrently using `Promise.all()`.
253− 
254−**Incorrect: sequential execution, 3 round trips**
255− 
256−```typescript
257−const user = await fetchUser()
258−const posts = await fetchPosts()
259−const comments = await fetchComments()
260−```
261− 
262−**Correct: parallel execution, 1 round trip**
263− 
264−```typescript
265−const [user, posts, comments] = await Promise.all([fetchUser(), fetchPosts(), fetchComments()])
266−```
267− 
268−### 1.5 Strategic Suspense Boundaries
269− 
270−**Impact: HIGH (faster initial paint)**
271− 
272−Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads.
273− 
274−**Incorrect: wrapper blocked by data fetching**
275− 
276−```tsx
277−async function Page() {
278− const data = await fetchData() // Blocks entire page
279− 
280− return (
281− <div>
282− <div>Sidebar</div>
283− <div>Header</div>
284− <div>
285− <DataDisplay data={data} />
286− </div>
287− <div>Footer</div>
288− </div>
289− )
290−}
291−```
292− 
293−The entire layout waits for data even though only the middle section needs it.
294− 
295−**Correct: wrapper shows immediately, data streams in**
296− 
297−```tsx
298−function Page() {
299− return (
300− <div>
301− <div>Sidebar</div>
302− <div>Header</div>
303− <div>
304− <Suspense fallback={<Skeleton />}>
305− <DataDisplay />
306− </Suspense>
307− </div>
308− <div>Footer</div>
309− </div>
310− )
311−}
312− 
313−async function DataDisplay() {
314− const data = await fetchData() // Only blocks this component
315− return <div>{data.content}</div>
316−}
317−```
318− 
319−Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data.
320− 
321−**Alternative: share promise across components**
322− 
323−```tsx
324−function Page() {
325− // Start fetch immediately, but don't await
326− const dataPromise = fetchData()
327− 
328− return (
329− <div>
330− <div>Sidebar</div>
331− <div>Header</div>
332− <Suspense fallback={<Skeleton />}>
333− <DataDisplay dataPromise={dataPromise} />
334− <DataSummary dataPromise={dataPromise} />
335− </Suspense>
336− <div>Footer</div>
337− </div>
338− )
339−}
340− 
341−function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {
342− const data = use(dataPromise) // Unwraps the promise
343− return <div>{data.content}</div>
344−}
345− 
346−function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {
347− const data = use(dataPromise) // Reuses the same promise
348− return <div>{data.summary}</div>
349−}
350−```
351− 
352−Both components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together.
353− 
354−**When NOT to use this pattern:**
355− 
356−- Critical data needed for layout decisions (affects positioning)
357− 
358−- SEO-critical content above the fold
359− 
360−- Small, fast queries where suspense overhead isn't worth it
361− 
362−- When you want to avoid layout shift (loading → content jump)
363− 
364−**Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities.
365− 
366−---
367− 
368−## 2. Bundle Size Optimization
369− 
370−**Impact: CRITICAL**
371− 
372−Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint.
373− 
374−### 2.1 Avoid Barrel File Imports
375− 
376−**Impact: CRITICAL (200-800ms import cost, slow builds)**
377− 
378−Import directly from source files instead of barrel files to avoid loading thousands of unused modules. **Barrel files** are entry points that re-export multiple modules (e.g., `index.js` that does `export * from './module'`).
379− 
380−Popular icon and component libraries can have **up to 10,000 re-exports** in their entry file. For many React packages, **it takes 200-800ms just to import them**, affecting both development speed and production cold starts.
381− 
382−**Why tree-shaking doesn't help:** When a library is marked as external (not bundled), the bundler can't optimize it. If you bundle it to enable tree-shaking, builds become substantially slower analyzing the entire module graph.
383− 
384−**Incorrect: imports entire library**
385− 
386−```tsx
387−import { Check, X, Menu } from "lucide-react"
388−// Loads 1,583 modules, takes ~2.8s extra in dev
389−// Runtime cost: 200-800ms on every cold start
390− 
391−import { Button, TextField } from "@mui/material"
392−// Loads 2,225 modules, takes ~4.2s extra in dev
393−```
394− 
395−**Correct: imports only what you need**
396− 
397−```tsx
398−import Check from "lucide-react/dist/esm/icons/check"
399−import X from "lucide-react/dist/esm/icons/x"
400−import Menu from "lucide-react/dist/esm/icons/menu"
401−// Loads only 3 modules (~2KB vs ~1MB)
402− 
403−import Button from "@mui/material/Button"
404−import TextField from "@mui/material/TextField"
405−// Loads only what you use
406−```
407− 
408−**Alternative: Next.js 13.5+**
409− 
410−```js
411−// next.config.js - use optimizePackageImports
412−module.exports = {
413− experimental: {
414− optimizePackageImports: ["lucide-react", "@mui/material"],
415− },
416−}
417− 
418−// Then you can keep the ergonomic barrel imports:
419−import { Check, X, Menu } from "lucide-react"
420−// Automatically transformed to direct imports at build time
421−```
422− 
423−Direct imports provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR.
424− 
425−Libraries commonly affected: `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `@headlessui/react`, `@radix-ui/react-*`, `lodash`, `ramda`, `date-fns`, `rxjs`, `react-use`.
426− 
427−Reference: [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)
428− 
429−### 2.2 Conditional Module Loading
430− 
431−**Impact: HIGH (loads large data only when needed)**
432− 
433−Load large data or modules only when a feature is activated.
434− 
435−**Example: lazy-load animation frames**
436− 
437−```tsx
438−function AnimationPlayer({
439− enabled,
440− setEnabled,
441−}: {
442− enabled: boolean
443− setEnabled: React.Dispatch<React.SetStateAction<boolean>>
444−}) {
445− const [frames, setFrames] = useState<Frame[] | null>(null)
446− 
447− useEffect(() => {
448− if (enabled && !frames && typeof window !== "undefined") {
449− import("./animation-frames.js")
450− .then((mod) => setFrames(mod.frames))
451− .catch(() => setEnabled(false))
452− }
453− }, [enabled, frames, setEnabled])
454− 
455− if (!frames) return <Skeleton />
456− return <Canvas frames={frames} />
457−}
458−```
459− 
460−The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed.
461− 
462−### 2.3 Defer Non-Critical Third-Party Libraries
463− 
464−**Impact: MEDIUM (loads after hydration)**
465− 
466−Analytics, logging, and error tracking don't block user interaction. Load them after hydration.
467− 
468−**Incorrect: blocks initial bundle**
469− 
470−```tsx
471−import { Analytics } from "@vercel/analytics/react"
472− 
473−export default function RootLayout({ children }) {
474− return (
475− <html>
476− <body>
477− {children}
478− <Analytics />
479− </body>
480− </html>
481− )
482−}
483−```
484− 
485−**Correct: loads after hydration**
486− 
487−```tsx
488−import dynamic from "next/dynamic"
489− 
490−const Analytics = dynamic(() => import("@vercel/analytics/react").then((m) => m.Analytics), {
491− ssr: false,
492−})
493− 
494−export default function RootLayout({ children }) {
495− return (
496− <html>
497− <body>
498− {children}
499− <Analytics />
500− </body>
501− </html>
502− )
503−}
504−```
505− 
506−### 2.4 Dynamic Imports for Heavy Components
507− 
508−**Impact: CRITICAL (directly affects TTI and LCP)**
509− 
510−Use `next/dynamic` to lazy-load large components not needed on initial render.
511− 
512−**Incorrect: Monaco bundles with main chunk ~300KB**
513− 
514−```tsx
515−import { MonacoEditor } from "./monaco-editor"
516− 
517−function CodePanel({ code }: { code: string }) {
518− return <MonacoEditor value={code} />
519−}
520−```
521− 
522−**Correct: Monaco loads on demand**
523− 
524−```tsx
525−import dynamic from "next/dynamic"
526− 
527−const MonacoEditor = dynamic(() => import("./monaco-editor").then((m) => m.MonacoEditor), {
528− ssr: false,
529−})
530− 
531−function CodePanel({ code }: { code: string }) {
532− return <MonacoEditor value={code} />
533−}
534−```
535− 
536−### 2.5 Preload Based on User Intent
537− 
538−**Impact: MEDIUM (reduces perceived latency)**
539− 
540−Preload heavy bundles before they're needed to reduce perceived latency.
541− 
542−**Example: preload on hover/focus**
543− 
544−```tsx
545−function EditorButton({ onClick }: { onClick: () => void }) {
546− const preload = () => {
547− if (typeof window !== "undefined") {
548− void import("./monaco-editor")
549− }
550− }
551− 
552− return (
553− <button onMouseEnter={preload} onFocus={preload} onClick={onClick}>
554− Open Editor
555− </button>
556− )
557−}
558−```
559− 
560−**Example: preload when feature flag is enabled**
561− 
562−```tsx
563−function FlagsProvider({ children, flags }: Props) {
564− useEffect(() => {
565− if (flags.editorEnabled && typeof window !== "undefined") {
566− void import("./monaco-editor").then((mod) => mod.init())
567− }
568− }, [flags.editorEnabled])
569− 
570− return <FlagsContext.Provider value={flags}>{children}</FlagsContext.Provider>
571−}
572−```
573− 
574−The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed.
575− 
576−---
577− 
578−## 3. Server-Side Performance
579− 
580−**Impact: HIGH**
581− 
582−Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times.
583− 
584−### 3.1 Authenticate Server Actions Like API Routes
585− 
586−**Impact: CRITICAL (prevents unauthorized access to server mutations)**
587− 
588−Server Actions (functions with `"use server"`) are exposed as public endpoints, just like API routes. Always verify authentication and authorization **inside** each Server Action—do not rely solely on middleware, layout guards, or page-level checks, as Server Actions can be invoked directly.
589− 
590−Next.js documentation explicitly states: "Treat Server Actions with the same security considerations as public-facing API endpoints, and verify if the user is allowed to perform a mutation."
591− 
592−**Incorrect: no authentication check**
593− 
594−```typescript
595−"use server"
596− 
597−export async function deleteUser(userId: string) {
598− // Anyone can call this! No auth check
599− await db.user.delete({ where: { id: userId } })
600− return { success: true }
601−}
602−```
603− 
604−**Correct: authentication inside the action**
605− 
606−```typescript
607−"use server"
608− 
609−import { verifySession } from "@/lib/auth"
610−import { unauthorized } from "@/lib/errors"
611− 
612−export async function deleteUser(userId: string) {
613− // Always check auth inside the action
614− const session = await verifySession()
615− 
616− if (!session) {
617− throw unauthorized("Must be logged in")
618− }
619− 
620− // Check authorization too
621− if (session.user.role !== "admin" && session.user.id !== userId) {
622− throw unauthorized("Cannot delete other users")
623− }
624− 
625− await db.user.delete({ where: { id: userId } })
626− return { success: true }
627−}
628−```
629− 
630−**With input validation:**
631− 
632−```typescript
633−"use server"
634− 
635−import { verifySession } from "@/lib/auth"
636−import { z } from "zod"
637− 
638−const updateProfileSchema = z.object({
639− userId: z.string().uuid(),
640− name: z.string().min(1).max(100),
641− email: z.string().email(),
642−})
643− 
644−export async function updateProfile(data: unknown) {
645− // Validate input first
646− const validated = updateProfileSchema.parse(data)
647− 
648− // Then authenticate
649− const session = await verifySession()
650− if (!session) {
651− throw new Error("Unauthorized")
652− }
653− 
654− // Then authorize
655− if (session.user.id !== validated.userId) {
656− throw new Error("Can only update own profile")
657− }
658− 
659− // Finally perform the mutation
660− await db.user.update({
661− where: { id: validated.userId },
662− data: {
663− name: validated.name,
664− email: validated.email,
665− },
666− })
667− 
668− return { success: true }
669−}
670−```
671− 
672−Reference: [https://nextjs.org/docs/app/guides/authentication](https://nextjs.org/docs/app/guides/authentication)
673− 
674−### 3.2 Avoid Duplicate Serialization in RSC Props
675− 
676−**Impact: LOW (reduces network payload by avoiding duplicate serialization)**
677− 
678−RSC→client serialization deduplicates by object reference, not value. Same reference = serialized once; new reference = serialized again. Do transformations (`.toSorted()`, `.filter()`, `.map()`) in client, not server.
679− 
680−**Incorrect: duplicates array**
681− 
682−```tsx
683−// RSC: sends 6 strings (2 arrays × 3 items)
684−<ClientList usernames={usernames} usernamesOrdered={usernames.toSorted()} />
685−```
686− 
687−**Correct: sends 3 strings**
688− 
689−```tsx
690−// RSC: send once
691−;<ClientList usernames={usernames} />
692− 
693−// Client: transform there
694−;("use client")
695−const sorted = useMemo(() => [...usernames].sort(), [usernames])
696−```
697− 
698−**Nested deduplication behavior:**
699− 
700−```tsx
701−// string[] - duplicates everything
702−usernames={['a','b']} sorted={usernames.toSorted()} // sends 4 strings
703− 
704−// object[] - duplicates array structure only
705−users={[{id:1},{id:2}]} sorted={users.toSorted()} // sends 2 arrays + 2 unique objects (not 4)
706−```
707− 
708−Deduplication works recursively. Impact varies by data type:
709− 
710−- `string[]`, `number[]`, `boolean[]`: **HIGH impact** - array + all primitives fully duplicated
711− 
712−- `object[]`: **LOW impact** - array duplicated, but nested objects deduplicated by reference
713− 
714−**Operations breaking deduplication: create new references**
715− 
716−- Arrays: `.toSorted()`, `.filter()`, `.map()`, `.slice()`, `[...arr]`
717− 
718−- Objects: `{...obj}`, `Object.assign()`, `structuredClone()`, `JSON.parse(JSON.stringify())`
719− 
720−**More examples:**
721− 
722−```tsx
723−// ❌ Bad
724−<C users={users} active={users.filter(u => u.active)} />
725−<C product={product} productName={product.name} />
726− 
727−// ✅ Good
728−<C users={users} />
729−<C product={product} />
730−// Do filtering/destructuring in client
731−```
732− 
733−**Exception:** Pass derived data when transformation is expensive or client doesn't need original.
734− 
735−### 3.3 Cross-Request LRU Caching
736− 
737−**Impact: HIGH (caches across requests)**
738− 
739−`React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache.
740− 
741−**Implementation:**
742− 
743−```typescript
744−import { LRUCache } from "lru-cache"
745− 
746−const cache = new LRUCache<string, any>({
747− max: 1000,
748− ttl: 5 * 60 * 1000, // 5 minutes
749−})
750− 
751−export async function getUser(id: string) {
752− const cached = cache.get(id)
753− if (cached) return cached
754− 
755− const user = await db.user.findUnique({ where: { id } })
756− cache.set(id, user)
757− return user
758−}
759− 
760−// Request 1: DB query, result cached
761−// Request 2: cache hit, no DB query
762−```
763− 
764−Use when sequential user actions hit multiple endpoints needing the same data within seconds.
765− 
766−**With Vercel's [Fluid Compute](https://vercel.com/docs/fluid-compute):** LRU caching is especially effective because multiple concurrent requests can share the same function instance and cache. This means the cache persists across requests without needing external storage like Redis.
767− 
768−**In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching.
769− 
770−Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)
771− 
772−### 3.4 Minimize Serialization at RSC Boundaries
773− 
774−**Impact: HIGH (reduces data transfer size)**
775− 
776−The React Server/Client boundary serializes all object properties into strings and embeds them in the HTML response and subsequent RSC requests. This serialized data directly impacts page weight and load time, so **size matters a lot**. Only pass fields that the client actually uses.
777− 
778−**Incorrect: serializes all 50 fields**
779− 
780−```tsx
781−async function Page() {
782− const user = await fetchUser() // 50 fields
783− return <Profile user={user} />
784−}
785− 
786−;("use client")
787−function Profile({ user }: { user: User }) {
788− return <div>{user.name}</div> // uses 1 field
789−}
790−```
791− 
792−**Correct: serializes only 1 field**
793− 
794−```tsx
795−async function Page() {
796− const user = await fetchUser()
797− return <Profile name={user.name} />
798−}
799− 
800−;("use client")
801−function Profile({ name }: { name: string }) {
802− return <div>{name}</div>
803−}
804−```
805− 
806−### 3.5 Parallel Data Fetching with Component Composition
807− 
808−**Impact: CRITICAL (eliminates server-side waterfalls)**
809− 
810−React Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching.
811− 
812−**Incorrect: Sidebar waits for Page's fetch to complete**
813− 
814−```tsx
815−export default async function Page() {
816− const header = await fetchHeader()
817− return (
818− <div>
819− <div>{header}</div>
820− <Sidebar />
821− </div>
822− )
823−}
824− 
825−async function Sidebar() {
826− const items = await fetchSidebarItems()
827− return <nav>{items.map(renderItem)}</nav>
828−}
829−```
830− 
831−**Correct: both fetch simultaneously**
832− 
833−```tsx
834−async function Header() {
835− const data = await fetchHeader()
836− return <div>{data}</div>
837−}
838− 
839−async function Sidebar() {
840− const items = await fetchSidebarItems()
841− return <nav>{items.map(renderItem)}</nav>
842−}
843− 
844−export default function Page() {
845− return (
846− <div>
847− <Header />
848− <Sidebar />
849− </div>
850− )
851−}
852−```
853− 
854−**Alternative with children prop:**
855− 
856−```tsx
857−async function Header() {
858− const data = await fetchHeader()
859− return <div>{data}</div>
860−}
861− 
862−async function Sidebar() {
863− const items = await fetchSidebarItems()
864− return <nav>{items.map(renderItem)}</nav>
865−}
866− 
867−function Layout({ children }: { children: ReactNode }) {
868− return (
869− <div>
870− <Header />
871− {children}
872− </div>
873− )
874−}
875− 
876−export default function Page() {
877− return (
878− <Layout>
879− <Sidebar />
880− </Layout>
881− )
882−}
883−```
884− 
885−### 3.6 Per-Request Deduplication with React.cache()
886− 
887−**Impact: MEDIUM (deduplicates within request)**
888− 
889−Use `React.cache()` for server-side request deduplication. Authentication and database queries benefit most.
890− 
891−**Usage:**
892− 
893−```typescript
894−import { cache } from "react"
895− 
896−export const getCurrentUser = cache(async () => {
897− const session = await auth()
898− if (!session?.user?.id) return null
899− return await db.user.findUnique({
900− where: { id: session.user.id },
901− })
902−})
903−```
904− 
905−Within a single request, multiple calls to `getCurrentUser()` execute the query only once.
906− 
907−**Avoid inline objects as arguments:**
908− 
909−`React.cache()` uses shallow equality (`Object.is`) to determine cache hits. Inline objects create new references each call, preventing cache hits.
910− 
911−**Incorrect: always cache miss**
912− 
913−```typescript
914−const getUser = cache(async (params: { uid: number }) => {
915− return await db.user.findUnique({ where: { id: params.uid } })
916−})
917− 
918−// Each call creates new object, never hits cache
919−getUser({ uid: 1 })
920−getUser({ uid: 1 }) // Cache miss, runs query again
921−```
922− 
923−**Correct: cache hit**
924− 
925−```typescript
926−const params = { uid: 1 }
927−getUser(params) // Query runs
928−getUser(params) // Cache hit (same reference)
929−```
930− 
931−If you must pass objects, pass the same reference:
932− 
933−**Next.js-Specific Note:**
934− 
935−In Next.js, the `fetch` API is automatically extended with request memoization. Requests with the same URL and options are automatically deduplicated within a single request, so you don't need `React.cache()` for `fetch` calls. However, `React.cache()` is still essential for other async tasks:
936− 
937−- Database queries (Prisma, Drizzle, etc.)
938− 
939−- Heavy computations
940− 
941−- Authentication checks
942− 
943−- File system operations
944− 
945−- Any non-fetch async work
946− 
947−Use `React.cache()` to deduplicate these operations across your component tree.
948− 
949−Reference: [https://react.dev/reference/react/cache](https://react.dev/reference/react/cache)
950− 
951−### 3.7 Use after() for Non-Blocking Operations
952− 
953−**Impact: MEDIUM (faster response times)**
954− 
955−Use Next.js's `after()` to schedule work that should execute after a response is sent. This prevents logging, analytics, and other side effects from blocking the response.
956− 
957−**Incorrect: blocks response**
958− 
959−```tsx
960−import { logUserAction } from "@/app/utils"
961− 
962−export async function POST(request: Request) {
963− // Perform mutation
964− await updateDatabase(request)
965− 
966− // Logging blocks the response
967− const userAgent = request.headers.get("user-agent") || "unknown"
968− await logUserAction({ userAgent })
969− 
970− return new Response(JSON.stringify({ status: "success" }), {
971− status: 200,
972− headers: { "Content-Type": "application/json" },
973− })
974−}
975−```
976− 
977−**Correct: non-blocking**
978− 
979−```tsx
980−import { after } from "next/server"
981−import { headers, cookies } from "next/headers"
982−import { logUserAction } from "@/app/utils"
983− 
984−export async function POST(request: Request) {
985− // Perform mutation
986− await updateDatabase(request)
987− 
988− // Log after response is sent
989− after(async () => {
990− const userAgent = (await headers()).get("user-agent") || "unknown"
991− const sessionCookie = (await cookies()).get("session-id")?.value || "anonymous"
992− 
993− logUserAction({ sessionCookie, userAgent })
994− })
995− 
996− return new Response(JSON.stringify({ status: "success" }), {
997− status: 200,
998− headers: { "Content-Type": "application/json" },
999− })
1000−}
1001−```
1002− 
1003−The response is sent immediately while logging happens in the background.
1004− 
1005−**Common use cases:**
1006− 
1007−- Analytics tracking
1008− 
1009−- Audit logging
1010− 
1011−- Sending notifications
1012− 
1013−- Cache invalidation
1014− 
1015−- Cleanup tasks
1016− 
1017−**Important notes:**
1018− 
1019−- `after()` runs even if the response fails or redirects
1020− 
1021−- Works in Server Actions, Route Handlers, and Server Components
1022− 
1023−Reference: [https://nextjs.org/docs/app/api-reference/functions/after](https://nextjs.org/docs/app/api-reference/functions/after)
1024− 
1025−---
1026− 
1027−## 4. Client-Side Data Fetching
1028− 
1029−**Impact: MEDIUM-HIGH**
1030− 
1031−Automatic deduplication and efficient data fetching patterns reduce redundant network requests.
1032− 
1033−### 4.1 Deduplicate Global Event Listeners
1034− 
1035−**Impact: LOW (single listener for N components)**
1036− 
1037−Use `useSWRSubscription()` to share global event listeners across component instances.
1038− 
1039−**Incorrect: N instances = N listeners**
1040− 
1041−```tsx
1042−function useKeyboardShortcut(key: string, callback: () => void) {
1043− useEffect(() => {
1044− const handler = (e: KeyboardEvent) => {
1045− if (e.metaKey && e.key === key) {
1046− callback()
1047− }
1048− }
1049− window.addEventListener("keydown", handler)
1050− return () => window.removeEventListener("keydown", handler)
1051− }, [key, callback])
1052−}
1053−```
1054− 
1055−When using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener.
1056− 
1057−**Correct: N instances = 1 listener**
1058− 
1059−```tsx
1060−import useSWRSubscription from "swr/subscription"
1061− 
1062−// Module-level Map to track callbacks per key
1063−const keyCallbacks = new Map<string, Set<() => void>>()
1064− 
1065−function useKeyboardShortcut(key: string, callback: () => void) {
1066− // Register this callback in the Map
1067− useEffect(() => {
1068− if (!keyCallbacks.has(key)) {
1069− keyCallbacks.set(key, new Set())
1070− }
1071− keyCallbacks.get(key)!.add(callback)
1072− 
1073− return () => {
1074− const set = keyCallbacks.get(key)
1075− if (set) {
1076− set.delete(callback)
1077− if (set.size === 0) {
1078− keyCallbacks.delete(key)
1079− }
1080− }
1081− }
1082− }, [key, callback])
1083− 
1084− useSWRSubscription("global-keydown", () => {
1085− const handler = (e: KeyboardEvent) => {
1086− if (e.metaKey && keyCallbacks.has(e.key)) {
1087− keyCallbacks.get(e.key)!.forEach((cb) => cb())
1088− }
1089− }
1090− window.addEventListener("keydown", handler)
1091− return () => window.removeEventListener("keydown", handler)
1092− })
1093−}
1094− 
1095−function Profile() {
1096− // Multiple shortcuts will share the same listener
1097− useKeyboardShortcut("p", () => {
1098− /* ... */
1099− })
1100− useKeyboardShortcut("k", () => {
1101− /* ... */
1102− })
1103− // ...
1104−}
1105−```
1106− 
1107−### 4.2 Use Passive Event Listeners for Scrolling Performance
1108− 
1109−**Impact: MEDIUM (eliminates scroll delay caused by event listeners)**
1110− 
1111−Add `{ passive: true }` to touch and wheel event listeners to enable immediate scrolling. Browsers normally wait for listeners to finish to check if `preventDefault()` is called, causing scroll delay.
1112− 
1113−**Incorrect:**
1114− 
1115−```typescript
1116−useEffect(() => {
1117− const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)
1118− const handleWheel = (e: WheelEvent) => console.log(e.deltaY)
1119− 
1120− document.addEventListener("touchstart", handleTouch)
1121− document.addEventListener("wheel", handleWheel)
1122− 
1123− return () => {
1124− document.removeEventListener("touchstart", handleTouch)
1125− document.removeEventListener("wheel", handleWheel)
1126− }
1127−}, [])
1128−```
1129− 
1130−**Correct:**
1131− 
1132−```typescript
1133−useEffect(() => {
1134− const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)
1135− const handleWheel = (e: WheelEvent) => console.log(e.deltaY)
1136− 
1137− document.addEventListener("touchstart", handleTouch, { passive: true })
1138− document.addEventListener("wheel", handleWheel, { passive: true })
1139− 
1140− return () => {
1141− document.removeEventListener("touchstart", handleTouch)
1142− document.removeEventListener("wheel", handleWheel)
1143− }
1144−}, [])
1145−```
1146− 
1147−**Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`.
1148− 
1149−**Don't use passive when:** implementing custom swipe gestures, custom zoom controls, or any listener that needs `preventDefault()`.
1150− 
1151−### 4.3 Use SWR for Automatic Deduplication
1152− 
1153−**Impact: MEDIUM-HIGH (automatic deduplication)**
1154− 
1155−SWR enables request deduplication, caching, and revalidation across component instances.
1156− 
1157−**Incorrect: no deduplication, each instance fetches**
1158− 
1159−```tsx
1160−function UserList() {
1161− const [users, setUsers] = useState([])
1162− useEffect(() => {
1163− fetch("/api/users")
1164− .then((r) => r.json())
1165− .then(setUsers)
1166− }, [])
1167−}
1168−```
1169− 
1170−**Correct: multiple instances share one request**
1171− 
1172−```tsx
1173−import useSWR from "swr"
1174− 
1175−function UserList() {
1176− const { data: users } = useSWR("/api/users", fetcher)
1177−}
1178−```
1179− 
1180−**For immutable data:**
1181− 
1182−```tsx
1183−import { useImmutableSWR } from "@/lib/swr"
1184− 
1185−function StaticContent() {
1186− const { data } = useImmutableSWR("/api/config", fetcher)
1187−}
1188−```
1189− 
1190−**For mutations:**
1191− 
1192−```tsx
1193−import { useSWRMutation } from "swr/mutation"
1194− 
1195−function UpdateButton() {
1196− const { trigger } = useSWRMutation("/api/user", updateUser)
1197− return <button onClick={() => trigger()}>Update</button>
1198−}
1199−```
1200− 
1201−Reference: [https://swr.vercel.app](https://swr.vercel.app)
1202− 
1203−### 4.4 Version and Minimize localStorage Data
1204− 
1205−**Impact: MEDIUM (prevents schema conflicts, reduces storage size)**
1206− 
1207−Add version prefix to keys and store only needed fields. Prevents schema conflicts and accidental storage of sensitive data.
1208− 
1209−**Incorrect:**
1210− 
1211−```typescript
1212−// No version, stores everything, no error handling
1213−localStorage.setItem("userConfig", JSON.stringify(fullUserObject))
1214−const data = localStorage.getItem("userConfig")
1215−```
1216− 
1217−**Correct:**
1218− 
1219−```typescript
1220−const VERSION = "v2"
1221− 
1222−function saveConfig(config: { theme: string; language: string }) {
1223− try {
1224− localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config))
1225− } catch {
1226− // Throws in incognito/private browsing, quota exceeded, or disabled
1227− }
1228−}
1229− 
1230−function loadConfig() {
1231− try {
1232− const data = localStorage.getItem(`userConfig:${VERSION}`)
1233− return data ? JSON.parse(data) : null
1234− } catch {
1235− return null
1236− }
1237−}
1238− 
1239−// Migration from v1 to v2
1240−function migrate() {
1241− try {
1242− const v1 = localStorage.getItem("userConfig:v1")
1243− if (v1) {
1244− const old = JSON.parse(v1)
1245− saveConfig({ theme: old.darkMode ? "dark" : "light", language: old.lang })
1246− localStorage.removeItem("userConfig:v1")
1247− }
1248− } catch {}
1249−}
1250−```
1251− 
1252−**Store minimal fields from server responses:**
1253− 
1254−```typescript
1255−// User object has 20+ fields, only store what UI needs
1256−function cachePrefs(user: FullUser) {
1257− try {
1258− localStorage.setItem(
1259− "prefs:v1",
1260− JSON.stringify({
1261− theme: user.preferences.theme,
1262− notifications: user.preferences.notifications,
1263− })
1264− )
1265− } catch {}
1266−}
1267−```
1268− 
1269−**Always wrap in try-catch:** `getItem()` and `setItem()` throw in incognito/private browsing (Safari, Firefox), when quota exceeded, or when disabled.
1270− 
1271−**Benefits:** Schema evolution via versioning, reduced storage size, prevents storing tokens/PII/internal flags.
1272− 
1273−---
1274− 
1275−## 5. Re-render Optimization
1276− 
1277−**Impact: MEDIUM**
1278− 
1279−Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness.
1280− 
1281−### 5.1 Calculate Derived State During Rendering
1282− 
1283−**Impact: MEDIUM (avoids redundant renders and state drift)**
1284− 
1285−If a value can be computed from current props/state, do not store it in state or update it in an effect. Derive it during render to avoid extra renders and state drift. Do not set state in effects solely in response to prop changes; prefer derived values or keyed resets instead.
1286− 
1287−**Incorrect: redundant state and effect**
1288− 
1289−```tsx
1290−function Form() {
1291− const [firstName, setFirstName] = useState("First")
1292− const [lastName, setLastName] = useState("Last")
1293− const [fullName, setFullName] = useState("")
1294− 
1295− useEffect(() => {
1296− setFullName(firstName + " " + lastName)
1297− }, [firstName, lastName])
1298− 
1299− return <p>{fullName}</p>
1300−}
1301−```
1302− 
1303−**Correct: derive during render**
1304− 
1305−```tsx
1306−function Form() {
1307− const [firstName, setFirstName] = useState("First")
1308− const [lastName, setLastName] = useState("Last")
1309− const fullName = firstName + " " + lastName
1310− 
1311− return <p>{fullName}</p>
1312−}
1313−```
1314− 
1315−Reference: [https://react.dev/learn/you-might-not-need-an-effect](https://react.dev/learn/you-might-not-need-an-effect)
1316− 
1317−### 5.2 Defer State Reads to Usage Point
1318− 
1319−**Impact: MEDIUM (avoids unnecessary subscriptions)**
1320− 
1321−Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks.
1322− 
1323−**Incorrect: subscribes to all searchParams changes**
1324− 
1325−```tsx
1326−function ShareButton({ chatId }: { chatId: string }) {
1327− const searchParams = useSearchParams()
1328− 
1329− const handleShare = () => {
1330− const ref = searchParams.get("ref")
1331− shareChat(chatId, { ref })
1332− }
1333− 
1334− return <button onClick={handleShare}>Share</button>
1335−}
1336−```
1337− 
1338−**Correct: reads on demand, no subscription**
1339− 
1340−```tsx
1341−function ShareButton({ chatId }: { chatId: string }) {
1342− const handleShare = () => {
1343− const params = new URLSearchParams(window.location.search)
1344− const ref = params.get("ref")
1345− shareChat(chatId, { ref })
1346− }
1347− 
1348− return <button onClick={handleShare}>Share</button>
1349−}
1350−```
1351− 
1352−### 5.3 Do not wrap a simple expression with a primitive result type in useMemo
1353− 
1354−**Impact: LOW-MEDIUM (wasted computation on every render)**
1355− 
1356−When an expression is simple (few logical or arithmetical operators) and has a primitive result type (boolean, number, string), do not wrap it in `useMemo`.
1357− 
1358−Calling `useMemo` and comparing hook dependencies may consume more resources than the expression itself.
1359− 
1360−**Incorrect:**
1361− 
1362−```tsx
1363−function Header({ user, notifications }: Props) {
1364− const isLoading = useMemo(() => {
1365− return user.isLoading || notifications.isLoading
1366− }, [user.isLoading, notifications.isLoading])
1367− 
1368− if (isLoading) return <Skeleton />
1369− // return some markup
1370−}
1371−```
1372− 
1373−**Correct:**
1374− 
1375−```tsx
1376−function Header({ user, notifications }: Props) {
1377− const isLoading = user.isLoading || notifications.isLoading
1378− 
1379− if (isLoading) return <Skeleton />
1380− // return some markup
1381−}
1382−```
1383− 
1384−### 5.4 Extract Default Non-primitive Parameter Value from Memoized Component to Constant
1385− 
1386−**Impact: MEDIUM (restores memoization by using a constant for default value)**
1387− 
1388−When memoized component has a default value for some non-primitive optional parameter, such as an array, function, or object, calling the component without that parameter results in broken memoization. This is because new value instances are created on every rerender, and they do not pass strict equality comparison in `memo()`.
1389− 
1390−To address this issue, extract the default value into a constant.
1391− 
1392−**Incorrect: `onClick` has different values on every rerender**
1393− 
1394−```tsx
1395−const UserAvatar = memo(function UserAvatar({ onClick = () => {} }: { onClick?: () => void }) {
1396− // ...
1397−})
1398− 
1399−// Used without optional onClick
1400−<UserAvatar />
1401−```
1402− 
1403−**Correct: stable default value**
1404− 
1405−```tsx
1406−const NOOP = () => {};
1407− 
1408−const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) {
1409− // ...
1410−})
1411− 
1412−// Used without optional onClick
1413−<UserAvatar />
1414−```
1415− 
1416−### 5.5 Extract to Memoized Components
1417− 
1418−**Impact: MEDIUM (enables early returns)**
1419− 
1420−Extract expensive work into memoized components to enable early returns before computation.
1421− 
1422−**Incorrect: computes avatar even when loading**
1423− 
1424−```tsx
1425−function Profile({ user, loading }: Props) {
1426− const avatar = useMemo(() => {
1427− const id = computeAvatarId(user)
1428− return <Avatar id={id} />
1429− }, [user])
1430− 
1431− if (loading) return <Skeleton />
1432− return <div>{avatar}</div>
1433−}
1434−```
1435− 
1436−**Correct: skips computation when loading**
1437− 
1438−```tsx
1439−const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
1440− const id = useMemo(() => computeAvatarId(user), [user])
1441− return <Avatar id={id} />
1442−})
1443− 
1444−function Profile({ user, loading }: Props) {
1445− if (loading) return <Skeleton />
1446− return (
1447− <div>
1448− <UserAvatar user={user} />
1449− </div>
1450− )
1451−}
1452−```
1453− 
1454−**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, manual memoization with `memo()` and `useMemo()` is not necessary. The compiler automatically optimizes re-renders.
1455− 
1456−### 5.6 Narrow Effect Dependencies
1457− 
1458−**Impact: LOW (minimizes effect re-runs)**
1459− 
1460−Specify primitive dependencies instead of objects to minimize effect re-runs.
1461− 
1462−**Incorrect: re-runs on any user field change**
1463− 
1464−```tsx
1465−useEffect(() => {
1466− console.log(user.id)
1467−}, [user])
1468−```
1469− 
1470−**Correct: re-runs only when id changes**
1471− 
1472−```tsx
1473−useEffect(() => {
1474− console.log(user.id)
1475−}, [user.id])
1476−```
1477− 
1478−**For derived state, compute outside effect:**
1479− 
1480−```tsx
1481−// Incorrect: runs on width=767, 766, 765...
1482−useEffect(() => {
1483− if (width < 768) {
1484− enableMobileMode()
1485− }
1486−}, [width])
1487− 
1488−// Correct: runs only on boolean transition
1489−const isMobile = width < 768
1490−useEffect(() => {
1491− if (isMobile) {
1492− enableMobileMode()
1493− }
1494−}, [isMobile])
1495−```
1496− 
1497−### 5.7 Put Interaction Logic in Event Handlers
1498− 
1499−**Impact: MEDIUM (avoids effect re-runs and duplicate side effects)**
1500− 
1501−If a side effect is triggered by a specific user action (submit, click, drag), run it in that event handler. Do not model the action as state + effect; it makes effects re-run on unrelated changes and can duplicate the action.
1502− 
1503−**Incorrect: event modeled as state + effect**
1504− 
1505−```tsx
1506−function Form() {
1507− const [submitted, setSubmitted] = useState(false)
1508− const theme = useContext(ThemeContext)
1509− 
1510− useEffect(() => {
1511− if (submitted) {
1512− post("/api/register")
1513− showToast("Registered", theme)
1514− }
1515− }, [submitted, theme])
1516− 
1517− return <button onClick={() => setSubmitted(true)}>Submit</button>
1518−}
1519−```
1520− 
1521−**Correct: do it in the handler**
1522− 
1523−```tsx
1524−function Form() {
1525− const theme = useContext(ThemeContext)
1526− 
1527− function handleSubmit() {
1528− post("/api/register")
1529− showToast("Registered", theme)
1530− }
1531− 
1532− return <button onClick={handleSubmit}>Submit</button>
1533−}
1534−```
1535− 
1536−Reference: [https://react.dev/learn/removing-effect-dependencies#should-this-code-move-to-an-event-handler](https://react.dev/learn/removing-effect-dependencies#should-this-code-move-to-an-event-handler)
1537− 
1538−### 5.8 Subscribe to Derived State
1539− 
1540−**Impact: MEDIUM (reduces re-render frequency)**
1541− 
1542−Subscribe to derived boolean state instead of continuous values to reduce re-render frequency.
1543− 
1544−**Incorrect: re-renders on every pixel change**
1545− 
1546−```tsx
1547−function Sidebar() {
1548− const width = useWindowWidth() // updates continuously
1549− const isMobile = width < 768
1550− return <nav className={isMobile ? "mobile" : "desktop"} />
1551−}
1552−```
1553− 
1554−**Correct: re-renders only when boolean changes**
1555− 
1556−```tsx
1557−function Sidebar() {
1558− const isMobile = useMediaQuery("(max-width: 767px)")
1559− return <nav className={isMobile ? "mobile" : "desktop"} />
1560−}
1561−```
1562− 
1563−### 5.9 Use Functional setState Updates
1564− 
1565−**Impact: MEDIUM (prevents stale closures and unnecessary callback recreations)**
1566− 
1567−When updating state based on the current state value, use the functional update form of setState instead of directly referencing the state variable. This prevents stale closures, eliminates unnecessary dependencies, and creates stable callback references.
1568− 
1569−**Incorrect: requires state as dependency**
1570− 
1571−```tsx
1572−function TodoList() {
1573− const [items, setItems] = useState(initialItems)
1574− 
1575− // Callback must depend on items, recreated on every items change
1576− const addItems = useCallback(
1577− (newItems: Item[]) => {
1578− setItems([...items, ...newItems])
1579− },
1580− [items]
1581− ) // ❌ items dependency causes recreations
1582− 
1583− // Risk of stale closure if dependency is forgotten
1584− const removeItem = useCallback((id: string) => {
1585− setItems(items.filter((item) => item.id !== id))
1586− }, []) // ❌ Missing items dependency - will use stale items!
1587− 
1588− return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
1589−}
1590−```
1591− 
1592−The first callback is recreated every time `items` changes, which can cause child components to re-render unnecessarily. The second callback has a stale closure bug—it will always reference the initial `items` value.
1593− 
1594−**Correct: stable callbacks, no stale closures**
1595− 
1596−```tsx
1597−function TodoList() {
1598− const [items, setItems] = useState(initialItems)
1599− 
1600− // Stable callback, never recreated
1601− const addItems = useCallback((newItems: Item[]) => {
1602− setItems((curr) => [...curr, ...newItems])
1603− }, []) // ✅ No dependencies needed
1604− 
1605− // Always uses latest state, no stale closure risk
1606− const removeItem = useCallback((id: string) => {
1607− setItems((curr) => curr.filter((item) => item.id !== id))
1608− }, []) // ✅ Safe and stable
1609− 
1610− return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
1611−}
1612−```
1613− 
1614−**Benefits:**
1615− 
1616−1. **Stable callback references** - Callbacks don't need to be recreated when state changes
1617− 
1618−2. **No stale closures** - Always operates on the latest state value
1619− 
1620−3. **Fewer dependencies** - Simplifies dependency arrays and reduces memory leaks
1621− 
1622−4. **Prevents bugs** - Eliminates the most common source of React closure bugs
1623− 
1624−**When to use functional updates:**
1625− 
1626−- Any setState that depends on the current state value
1627− 
1628−- Inside useCallback/useMemo when state is needed
1629− 
1630−- Event handlers that reference state
1631− 
1632−- Async operations that update state
1633− 
1634−**When direct updates are fine:**
1635− 
1636−- Setting state to a static value: `setCount(0)`
1637− 
1638−- Setting state from props/arguments only: `setName(newName)`
1639− 
1640−- State doesn't depend on previous value
1641− 
1642−**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler can automatically optimize some cases, but functional updates are still recommended for correctness and to prevent stale closure bugs.
1643− 
1644−### 5.10 Use Lazy State Initialization
1645− 
1646−**Impact: MEDIUM (wasted computation on every render)**
1647− 
1648−Pass a function to `useState` for expensive initial values. Without the function form, the initializer runs on every render even though the value is only used once.
1649− 
1650−**Incorrect: runs on every render**
1651− 
1652−```tsx
1653−function FilteredList({ items }: { items: Item[] }) {
1654− // buildSearchIndex() runs on EVERY render, even after initialization
1655− const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))
1656− const [query, setQuery] = useState("")
1657− 
1658− // When query changes, buildSearchIndex runs again unnecessarily
1659− return <SearchResults index={searchIndex} query={query} />
1660−}
1661− 
1662−function UserProfile() {
1663− // JSON.parse runs on every render
1664− const [settings, setSettings] = useState(JSON.parse(localStorage.getItem("settings") || "{}"))
1665− 
1666− return <SettingsForm settings={settings} onChange={setSettings} />
1667−}
1668−```
1669− 
1670−**Correct: runs only once**
1671− 
1672−```tsx
1673−function FilteredList({ items }: { items: Item[] }) {
1674− // buildSearchIndex() runs ONLY on initial render
1675− const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))
1676− const [query, setQuery] = useState("")
1677− 
1678− return <SearchResults index={searchIndex} query={query} />
1679−}
1680− 
1681−function UserProfile() {
1682− // JSON.parse runs only on initial render
1683− const [settings, setSettings] = useState(() => {
1684− const stored = localStorage.getItem("settings")
1685− return stored ? JSON.parse(stored) : {}
1686− })
1687− 
1688− return <SettingsForm settings={settings} onChange={setSettings} />
1689−}
1690−```
1691− 
1692−Use lazy initialization when computing initial values from localStorage/sessionStorage, building data structures (indexes, maps), reading from the DOM, or performing heavy transformations.
1693− 
1694−For simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary.
1695− 
1696−### 5.11 Use Transitions for Non-Urgent Updates
1697− 
1698−**Impact: MEDIUM (maintains UI responsiveness)**
1699− 
1700−Mark frequent, non-urgent state updates as transitions to maintain UI responsiveness.
1701− 
1702−**Incorrect: blocks UI on every scroll**
1703− 
1704−```tsx
1705−function ScrollTracker() {
1706− const [scrollY, setScrollY] = useState(0)
1707− useEffect(() => {
1708− const handler = () => setScrollY(window.scrollY)
1709− window.addEventListener("scroll", handler, { passive: true })
1710− return () => window.removeEventListener("scroll", handler)
1711− }, [])
1712−}
1713−```
1714− 
1715−**Correct: non-blocking updates**
1716− 
1717−```tsx
1718−import { startTransition } from "react"
1719− 
1720−function ScrollTracker() {
1721− const [scrollY, setScrollY] = useState(0)
1722− useEffect(() => {
1723− const handler = () => {
1724− startTransition(() => setScrollY(window.scrollY))
1725− }
1726− window.addEventListener("scroll", handler, { passive: true })
1727− return () => window.removeEventListener("scroll", handler)
1728− }, [])
1729−}
1730−```
1731− 
1732−### 5.12 Use useRef for Transient Values
1733− 
1734−**Impact: MEDIUM (avoids unnecessary re-renders on frequent updates)**
1735− 
1736−When a value changes frequently and you don't want a re-render on every update (e.g., mouse trackers, intervals, transient flags), store it in `useRef` instead of `useState`. Keep component state for UI; use refs for temporary DOM-adjacent values. Updating a ref does not trigger a re-render.
1737− 
1738−**Incorrect: renders every update**
1739− 
1740−```tsx
1741−function Tracker() {
1742− const [lastX, setLastX] = useState(0)
1743− 
1744− useEffect(() => {
1745− const onMove = (e: MouseEvent) => setLastX(e.clientX)
1746− window.addEventListener("mousemove", onMove)
1747− return () => window.removeEventListener("mousemove", onMove)
1748− }, [])
1749− 
1750− return (
1751− <div
1752− style={{
1753− position: "fixed",
1754− top: 0,
1755− left: lastX,
1756− width: 8,
1757− height: 8,
1758− background: "black",
1759− }}
1760− />
1761− )
1762−}
1763−```
1764− 
1765−**Correct: no re-render for tracking**
1766− 
1767−```tsx
1768−function Tracker() {
1769− const lastXRef = useRef(0)
1770− const dotRef = useRef<HTMLDivElement>(null)
1771− 
1772− useEffect(() => {
1773− const onMove = (e: MouseEvent) => {
1774− lastXRef.current = e.clientX
1775− const node = dotRef.current
1776− if (node) {
1777− node.style.transform = `translateX(${e.clientX}px)`
1778− }
1779− }
1780− window.addEventListener("mousemove", onMove)
1781− return () => window.removeEventListener("mousemove", onMove)
1782− }, [])
1783− 
1784− return (
1785− <div
1786− ref={dotRef}
1787− style={{
1788− position: "fixed",
1789− top: 0,
1790− left: 0,
1791− width: 8,
1792− height: 8,
1793− background: "black",
1794− transform: "translateX(0px)",
1795− }}
1796− />
1797− )
1798−}
1799−```
1800− 
1801−---
1802− 
1803−## 6. Rendering Performance
1804− 
1805−**Impact: MEDIUM**
1806− 
1807−Optimizing the rendering process reduces the work the browser needs to do.
1808− 
1809−### 6.1 Animate SVG Wrapper Instead of SVG Element
1810− 
1811−**Impact: LOW (enables hardware acceleration)**
1812− 
1813−Many browsers don't have hardware acceleration for CSS3 animations on SVG elements. Wrap SVG in a `<div>` and animate the wrapper instead.
1814− 
1815−**Incorrect: animating SVG directly - no hardware acceleration**
1816− 
1817−```tsx
1818−function LoadingSpinner() {
1819− return (
1820− <svg className="animate-spin" width="24" height="24" viewBox="0 0 24 24">
1821− <circle cx="12" cy="12" r="10" stroke="currentColor" />
1822− </svg>
1823− )
1824−}
1825−```
1826− 
1827−**Correct: animating wrapper div - hardware accelerated**
1828− 
1829−```tsx
1830−function LoadingSpinner() {
1831− return (
1832− <div className="animate-spin">
1833− <svg width="24" height="24" viewBox="0 0 24 24">
1834− <circle cx="12" cy="12" r="10" stroke="currentColor" />
1835− </svg>
1836− </div>
1837− )
1838−}
1839−```
1840− 
1841−This applies to all CSS transforms and transitions (`transform`, `opacity`, `translate`, `scale`, `rotate`). The wrapper div allows browsers to use GPU acceleration for smoother animations.
1842− 
1843−### 6.2 CSS content-visibility for Long Lists
1844− 
1845−**Impact: HIGH (faster initial render)**
1846− 
1847−Apply `content-visibility: auto` to defer off-screen rendering.
1848− 
1849−**CSS:**
1850− 
1851−```css
1852−.message-item {
1853− content-visibility: auto;
1854− contain-intrinsic-size: 0 80px;
1855−}
1856−```
1857− 
1858−**Example:**
1859− 
1860−```tsx
1861−function MessageList({ messages }: { messages: Message[] }) {
1862− return (
1863− <div className="h-screen overflow-y-auto">
1864− {messages.map((msg) => (
1865− <div key={msg.id} className="message-item">
1866− <Avatar user={msg.author} />
1867− <div>{msg.content}</div>
1868− </div>
1869− ))}
1870− </div>
1871− )
1872−}
1873−```
1874− 
1875−For 1000 messages, browser skips layout/paint for ~990 off-screen items (10× faster initial render).
1876− 
1877−### 6.3 Hoist Static JSX Elements
1878− 
1879−**Impact: LOW (avoids re-creation)**
1880− 
1881−Extract static JSX outside components to avoid re-creation.
1882− 
1883−**Incorrect: recreates element every render**
1884− 
1885−```tsx
1886−function LoadingSkeleton() {
1887− return <div className="h-20 animate-pulse bg-gray-200" />
1888−}
1889− 
1890−function Container() {
1891− return <div>{loading && <LoadingSkeleton />}</div>
1892−}
1893−```
1894− 
1895−**Correct: reuses same element**
1896− 
1897−```tsx
1898−const loadingSkeleton = <div className="h-20 animate-pulse bg-gray-200" />
1899− 
1900−function Container() {
1901− return <div>{loading && loadingSkeleton}</div>
1902−}
1903−```
1904− 
1905−This is especially helpful for large and static SVG nodes, which can be expensive to recreate on every render.
1906− 
1907−**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler automatically hoists static JSX elements and optimizes component re-renders, making manual hoisting unnecessary.
1908− 
1909−### 6.4 Optimize SVG Precision
1910− 
1911−**Impact: LOW (reduces file size)**
1912− 
1913−Reduce SVG coordinate precision to decrease file size. The optimal precision depends on the viewBox size, but in general reducing precision should be considered.
1914− 
1915−**Incorrect: excessive precision**
1916− 
1917−```svg
1918−<path d="M 10.293847 20.847362 L 30.938472 40.192837" />
1919−```
1920− 
1921−**Correct: 1 decimal place**
1922− 
1923−```svg
1924−<path d="M 10.3 20.8 L 30.9 40.2" />
1925−```
1926− 
1927−**Automate with SVGO:**
1928− 
1929−```bash
1930−npx svgo --precision=1 --multipass icon.svg
1931−```
1932− 
1933−### 6.5 Prevent Hydration Mismatch Without Flickering
1934− 
1935−**Impact: MEDIUM (avoids visual flicker and hydration errors)**
1936− 
1937−When rendering content that depends on client-side storage (localStorage, cookies), avoid both SSR breakage and post-hydration flickering by injecting a synchronous script that updates the DOM before React hydrates.
1938− 
1939−**Incorrect: breaks SSR**
1940− 
1941−```tsx
1942−function ThemeWrapper({ children }: { children: ReactNode }) {
1943− // localStorage is not available on server - throws error
1944− const theme = localStorage.getItem("theme") || "light"
1945− 
1946− return <div className={theme}>{children}</div>
1947−}
1948−```
1949− 
1950−Server-side rendering will fail because `localStorage` is undefined.
1951− 
1952−**Incorrect: visual flickering**
1953− 
1954−```tsx
1955−function ThemeWrapper({ children }: { children: ReactNode }) {
1956− const [theme, setTheme] = useState("light")
1957− 
1958− useEffect(() => {
1959− // Runs after hydration - causes visible flash
1960− const stored = localStorage.getItem("theme")
1961− if (stored) {
1962− setTheme(stored)
1963− }
1964− }, [])
1965− 
1966− return <div className={theme}>{children}</div>
1967−}
1968−```
1969− 
1970−Component first renders with default value (`light`), then updates after hydration, causing a visible flash of incorrect content.
1971− 
1972−**Correct: no flicker, no hydration mismatch**
1973− 
1974−```tsx
1975−function ThemeWrapper({ children }: { children: ReactNode }) {
1976− return (
1977− <>
1978− <div id="theme-wrapper">{children}</div>
1979− <script
1980− dangerouslySetInnerHTML={{
1981− __html: `
1982− (function() {
1983− try {
1984− var theme = localStorage.getItem('theme') || 'light';
1985− var el = document.getElementById('theme-wrapper');
1986− if (el) el.className = theme;
1987− } catch (e) {}
1988− })();
1989− `,
1990− }}
1991− />
1992− </>
1993− )
1994−}
1995−```
1996− 
1997−The inline script executes synchronously before showing the element, ensuring the DOM already has the correct value. No flickering, no hydration mismatch.
1998− 
1999−This pattern is especially useful for theme toggles, user preferences, authentication states, and any client-only data that should render immediately without flashing default values.
2000− 
2001−### 6.6 Suppress Expected Hydration Mismatches
2002− 
2003−**Impact: LOW-MEDIUM (avoids noisy hydration warnings for known differences)**
2004− 
2005−In SSR frameworks (e.g., Next.js), some values are intentionally different on server vs client (random IDs, dates, locale/timezone formatting). For these _expected_ mismatches, wrap the dynamic text in an element with `suppressHydrationWarning` to prevent noisy warnings. Do not use this to hide real bugs. Don’t overuse it.
2006− 
2007−**Incorrect: known mismatch warnings**
2008− 
2009−```tsx
2010−function Timestamp() {
2011− return <span>{new Date().toLocaleString()}</span>
2012−}
2013−```
2014− 
2015−**Correct: suppress expected mismatch only**
2016− 
2017−```tsx
2018−function Timestamp() {
2019− return <span suppressHydrationWarning>{new Date().toLocaleString()}</span>
2020−}
2021−```
2022− 
2023−### 6.7 Use Activity Component for Show/Hide
2024− 
2025−**Impact: MEDIUM (preserves state/DOM)**
2026− 
2027−Use React's `<Activity>` to preserve state/DOM for expensive components that frequently toggle visibility.
2028− 
2029−**Usage:**
2030− 
2031−```tsx
2032−import { Activity } from "react"
2033− 
2034−function Dropdown({ isOpen }: Props) {
2035− return (
2036− <Activity mode={isOpen ? "visible" : "hidden"}>
2037− <ExpensiveMenu />
2038− </Activity>
2039− )
2040−}
2041−```
2042− 
2043−Avoids expensive re-renders and state loss.
2044− 
2045−### 6.8 Use Explicit Conditional Rendering
2046− 
2047−**Impact: LOW (prevents rendering 0 or NaN)**
2048− 
2049−Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render.
2050− 
2051−**Incorrect: renders "0" when count is 0**
2052− 
2053−```tsx
2054−function Badge({ count }: { count: number }) {
2055− return <div>{count && <span className="badge">{count}</span>}</div>
2056−}
2057− 
2058−// When count = 0, renders: <div>0</div>
2059−// When count = 5, renders: <div><span class="badge">5</span></div>
2060−```
2061− 
2062−**Correct: renders nothing when count is 0**
2063− 
2064−```tsx
2065−function Badge({ count }: { count: number }) {
2066− return <div>{count > 0 ? <span className="badge">{count}</span> : null}</div>
2067−}
2068− 
2069−// When count = 0, renders: <div></div>
2070−// When count = 5, renders: <div><span class="badge">5</span></div>
2071−```
2072− 
2073−### 6.9 Use useTransition Over Manual Loading States
2074− 
2075−**Impact: LOW (reduces re-renders and improves code clarity)**
2076− 
2077−Use `useTransition` instead of manual `useState` for loading states. This provides built-in `isPending` state and automatically manages transitions.
2078− 
2079−**Incorrect: manual loading state**
2080− 
2081−```tsx
2082−function SearchResults() {
2083− const [query, setQuery] = useState("")
2084− const [results, setResults] = useState([])
2085− const [isLoading, setIsLoading] = useState(false)
2086− 
2087− const handleSearch = async (value: string) => {
2088− setIsLoading(true)
2089− setQuery(value)
2090− const data = await fetchResults(value)
2091− setResults(data)
2092− setIsLoading(false)
2093− }
2094− 
2095− return (
2096− <>
2097− <input onChange={(e) => handleSearch(e.target.value)} />
2098− {isLoading && <Spinner />}
2099− <ResultsList results={results} />
2100− </>
2101− )
2102−}
2103−```
2104− 
2105−**Correct: useTransition with built-in pending state**
2106− 
2107−```tsx
2108−import { useTransition, useState } from "react"
2109− 
2110−function SearchResults() {
2111− const [query, setQuery] = useState("")
2112− const [results, setResults] = useState([])
2113− const [isPending, startTransition] = useTransition()
2114− 
2115− const handleSearch = (value: string) => {
2116− setQuery(value) // Update input immediately
2117− 
2118− startTransition(async () => {
2119− // Fetch and update results
2120− const data = await fetchResults(value)
2121− setResults(data)
2122− })
2123− }
2124− 
2125− return (
2126− <>
2127− <input onChange={(e) => handleSearch(e.target.value)} />
2128− {isPending && <Spinner />}
2129− <ResultsList results={results} />
2130− </>
2131− )
2132−}
2133−```
2134− 
2135−**Benefits:**
2136− 
2137−- **Automatic pending state**: No need to manually manage `setIsLoading(true/false)`
2138− 
2139−- **Error resilience**: Pending state correctly resets even if the transition throws
2140− 
2141−- **Better responsiveness**: Keeps the UI responsive during updates
2142− 
2143−- **Interrupt handling**: New transitions automatically cancel pending ones
2144− 
2145−Reference: [https://react.dev/reference/react/useTransition](https://react.dev/reference/react/useTransition)
2146− 
2147−---
2148− 
2149−## 7. JavaScript Performance
2150− 
2151−**Impact: LOW-MEDIUM**
2152− 
2153−Micro-optimizations for hot paths can add up to meaningful improvements.
2154− 
2155−### 7.1 Avoid Layout Thrashing
2156− 
2157−**Impact: MEDIUM (prevents forced synchronous layouts and reduces performance bottlenecks)**
2158− 
2159−Avoid interleaving style writes with layout reads. When you read a layout property (like `offsetWidth`, `getBoundingClientRect()`, or `getComputedStyle()`) between style changes, the browser is forced to trigger a synchronous reflow.
2160− 
2161−**This is OK: browser batches style changes**
2162− 
2163−```typescript
2164−function updateElementStyles(element: HTMLElement) {
2165− // Each line invalidates style, but browser batches the recalculation
2166− element.style.width = "100px"
2167− element.style.height = "200px"
2168− element.style.backgroundColor = "blue"
2169− element.style.border = "1px solid black"
2170−}
2171−```
2172− 
2173−**Incorrect: interleaved reads and writes force reflows**
2174− 
2175−```typescript
2176−function layoutThrashing(element: HTMLElement) {
2177− element.style.width = "100px"
2178− const width = element.offsetWidth // Forces reflow
2179− element.style.height = "200px"
2180− const height = element.offsetHeight // Forces another reflow
2181−}
2182−```
2183− 
2184−**Correct: batch writes, then read once**
2185− 
2186−```typescript
2187−function updateElementStyles(element: HTMLElement) {
2188− // Batch all writes together
2189− element.style.width = "100px"
2190− element.style.height = "200px"
2191− element.style.backgroundColor = "blue"
2192− element.style.border = "1px solid black"
2193− 
2194− // Read after all writes are done (single reflow)
2195− const { width, height } = element.getBoundingClientRect()
2196−}
2197−```
2198− 
2199−**Correct: batch reads, then writes**
2200− 
2201−```typescript
2202−function updateElementStyles(element: HTMLElement) {
2203− element.classList.add("highlighted-box")
2204− 
2205− const { width, height } = element.getBoundingClientRect()
2206−}
2207−```
2208− 
2209−**Better: use CSS classes**
2210− 
2211−**React example:**
2212− 
2213−```tsx
2214−// Incorrect: interleaving style changes with layout queries
2215−function Box({ isHighlighted }: { isHighlighted: boolean }) {
2216− const ref = useRef<HTMLDivElement>(null)
2217− 
2218− useEffect(() => {
2219− if (ref.current && isHighlighted) {
2220− ref.current.style.width = "100px"
2221− const width = ref.current.offsetWidth // Forces layout
2222− ref.current.style.height = "200px"
2223− }
2224− }, [isHighlighted])
2225− 
2226− return <div ref={ref}>Content</div>
2227−}
2228− 
2229−// Correct: toggle class
2230−function Box({ isHighlighted }: { isHighlighted: boolean }) {
2231− return <div className={isHighlighted ? "highlighted-box" : ""}>Content</div>
2232−}
2233−```
2234− 
2235−Prefer CSS classes over inline styles when possible. CSS files are cached by the browser, and classes provide better separation of concerns and are easier to maintain.
2236− 
2237−See [this gist](https://gist.github.com/paulirish/5d52fb081b3570c81e3a) and [CSS Triggers](https://csstriggers.com/) for more information on layout-forcing operations.
2238− 
2239−### 7.2 Build Index Maps for Repeated Lookups
2240− 
2241−**Impact: LOW-MEDIUM (1M ops to 2K ops)**
2242− 
2243−Multiple `.find()` calls by the same key should use a Map.
2244− 
2245−**Incorrect (O(n) per lookup):**
2246− 
2247−```typescript
2248−function processOrders(orders: Order[], users: User[]) {
2249− return orders.map((order) => ({
2250− ...order,
2251− user: users.find((u) => u.id === order.userId),
2252− }))
2253−}
2254−```
2255− 
2256−**Correct (O(1) per lookup):**
2257− 
2258−```typescript
2259−function processOrders(orders: Order[], users: User[]) {
2260− const userById = new Map(users.map((u) => [u.id, u]))
2261− 
2262− return orders.map((order) => ({
2263− ...order,
2264− user: userById.get(order.userId),
2265− }))
2266−}
2267−```
2268− 
2269−Build map once (O(n)), then all lookups are O(1).
2270− 
2271−For 1000 orders × 1000 users: 1M ops → 2K ops.
2272− 
2273−### 7.3 Cache Property Access in Loops
2274− 
2275−**Impact: LOW-MEDIUM (reduces lookups)**
2276− 
2277−Cache object property lookups in hot paths.
2278− 
2279−**Incorrect: 3 lookups × N iterations**
2280− 
2281−```typescript
2282−for (let i = 0; i < arr.length; i++) {
2283− process(obj.config.settings.value)
2284−}
2285−```
2286− 
2287−**Correct: 1 lookup total**
2288− 
2289−```typescript
2290−const value = obj.config.settings.value
2291−const len = arr.length
2292−for (let i = 0; i < len; i++) {
2293− process(value)
2294−}
2295−```
2296− 
2297−### 7.4 Cache Repeated Function Calls
2298− 
2299−**Impact: MEDIUM (avoid redundant computation)**
2300− 
2301−Use a module-level Map to cache function results when the same function is called repeatedly with the same inputs during render.
2302− 
2303−**Incorrect: redundant computation**
2304− 
2305−```typescript
2306−function ProjectList({ projects }: { projects: Project[] }) {
2307− return (
2308− <div>
2309− {projects.map(project => {
2310− // slugify() called 100+ times for same project names
2311− const slug = slugify(project.name)
2312− 
2313− return <ProjectCard key={project.id} slug={slug} />
2314− })}
2315− </div>
2316− )
2317−}
2318−```
2319− 
2320−**Correct: cached results**
2321− 
2322−```typescript
2323−// Module-level cache
2324−const slugifyCache = new Map<string, string>()
2325− 
2326−function cachedSlugify(text: string): string {
2327− if (slugifyCache.has(text)) {
2328− return slugifyCache.get(text)!
2329− }
2330− const result = slugify(text)
2331− slugifyCache.set(text, result)
2332− return result
2333−}
2334− 
2335−function ProjectList({ projects }: { projects: Project[] }) {
2336− return (
2337− <div>
2338− {projects.map(project => {
2339− // Computed only once per unique project name
2340− const slug = cachedSlugify(project.name)
2341− 
2342− return <ProjectCard key={project.id} slug={slug} />
2343− })}
2344− </div>
2345− )
2346−}
2347−```
2348− 
2349−**Simpler pattern for single-value functions:**
2350− 
2351−```typescript
2352−let isLoggedInCache: boolean | null = null
2353− 
2354−function isLoggedIn(): boolean {
2355− if (isLoggedInCache !== null) {
2356− return isLoggedInCache
2357− }
2358− 
2359− isLoggedInCache = document.cookie.includes("auth=")
2360− return isLoggedInCache
2361−}
2362− 
2363−// Clear cache when auth changes
2364−function onAuthChange() {
2365− isLoggedInCache = null
2366−}
2367−```
2368− 
2369−Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.
2370− 
2371−Reference: [https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast)
2372− 
2373−### 7.5 Cache Storage API Calls
2374− 
2375−**Impact: LOW-MEDIUM (reduces expensive I/O)**
2376− 
2377−`localStorage`, `sessionStorage`, and `document.cookie` are synchronous and expensive. Cache reads in memory.
2378− 
2379−**Incorrect: reads storage on every call**
2380− 
2381−```typescript
2382−function getTheme() {
2383− return localStorage.getItem("theme") ?? "light"
2384−}
2385−// Called 10 times = 10 storage reads
2386−```
2387− 
2388−**Correct: Map cache**
2389− 
2390−```typescript
2391−const storageCache = new Map<string, string | null>()
2392− 
2393−function getLocalStorage(key: string) {
2394− if (!storageCache.has(key)) {
2395− storageCache.set(key, localStorage.getItem(key))
2396− }
2397− return storageCache.get(key)
2398−}
2399− 
2400−function setLocalStorage(key: string, value: string) {
2401− localStorage.setItem(key, value)
2402− storageCache.set(key, value) // keep cache in sync
2403−}
2404−```
2405− 
2406−Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.
2407− 
2408−**Cookie caching:**
2409− 
2410−```typescript
2411−let cookieCache: Record<string, string> | null = null
2412− 
2413−function getCookie(name: string) {
2414− if (!cookieCache) {
2415− cookieCache = Object.fromEntries(document.cookie.split("; ").map((c) => c.split("=")))
2416− }
2417− return cookieCache[name]
2418−}
2419−```
2420− 
2421−**Important: invalidate on external changes**
2422− 
2423−```typescript
2424−window.addEventListener("storage", (e) => {
2425− if (e.key) storageCache.delete(e.key)
2426−})
2427− 
2428−document.addEventListener("visibilitychange", () => {
2429− if (document.visibilityState === "visible") {
2430− storageCache.clear()
2431− }
2432−})
2433−```
2434− 
2435−If storage can change externally (another tab, server-set cookies), invalidate cache:
2436− 
2437−### 7.6 Combine Multiple Array Iterations
2438− 
2439−**Impact: LOW-MEDIUM (reduces iterations)**
2440− 
2441−Multiple `.filter()` or `.map()` calls iterate the array multiple times. Combine into one loop.
2442− 
2443−**Incorrect: 3 iterations**
2444− 
2445−```typescript
2446−const admins = users.filter((u) => u.isAdmin)
2447−const testers = users.filter((u) => u.isTester)
2448−const inactive = users.filter((u) => !u.isActive)
2449−```
2450− 
2451−**Correct: 1 iteration**
2452− 
2453−```typescript
2454−const admins: User[] = []
2455−const testers: User[] = []
2456−const inactive: User[] = []
2457− 
2458−for (const user of users) {
2459− if (user.isAdmin) admins.push(user)
2460− if (user.isTester) testers.push(user)
2461− if (!user.isActive) inactive.push(user)
2462−}
2463−```
2464− 
2465−### 7.7 Early Length Check for Array Comparisons
2466− 
2467−**Impact: MEDIUM-HIGH (avoids expensive operations when lengths differ)**
2468− 
2469−When comparing arrays with expensive operations (sorting, deep equality, serialization), check lengths first. If lengths differ, the arrays cannot be equal.
2470− 
2471−In real-world applications, this optimization is especially valuable when the comparison runs in hot paths (event handlers, render loops).
2472− 
2473−**Incorrect: always runs expensive comparison**
2474− 
2475−```typescript
2476−function hasChanges(current: string[], original: string[]) {
2477− // Always sorts and joins, even when lengths differ
2478− return current.sort().join() !== original.sort().join()
2479−}
2480−```
2481− 
2482−Two O(n log n) sorts run even when `current.length` is 5 and `original.length` is 100. There is also overhead of joining the arrays and comparing the strings.
2483− 
2484−**Correct (O(1) length check first):**
2485− 
2486−```typescript
2487−function hasChanges(current: string[], original: string[]) {
2488− // Early return if lengths differ
2489− if (current.length !== original.length) {
2490− return true
2491− }
2492− // Only sort when lengths match
2493− const currentSorted = current.toSorted()
2494− const originalSorted = original.toSorted()
2495− for (let i = 0; i < currentSorted.length; i++) {
2496− if (currentSorted[i] !== originalSorted[i]) {
2497− return true
2498− }
2499− }
2500− return false
2501−}
2502−```
2503− 
2504−This new approach is more efficient because:
2505− 
2506−- It avoids the overhead of sorting and joining the arrays when lengths differ
2507− 
2508−- It avoids consuming memory for the joined strings (especially important for large arrays)
2509− 
2510−- It avoids mutating the original arrays
2511− 
2512−- It returns early when a difference is found
2513− 
2514−### 7.8 Early Return from Functions
2515− 
2516−**Impact: LOW-MEDIUM (avoids unnecessary computation)**
2517− 
2518−Return early when result is determined to skip unnecessary processing.
2519− 
2520−**Incorrect: processes all items even after finding answer**
2521− 
2522−```typescript
2523−function validateUsers(users: User[]) {
2524− let hasError = false
2525− let errorMessage = ""
2526− 
2527− for (const user of users) {
2528− if (!user.email) {
2529− hasError = true
2530− errorMessage = "Email required"
2531− }
2532− if (!user.name) {
2533− hasError = true
2534− errorMessage = "Name required"
2535− }
2536− // Continues checking all users even after error found
2537− }
2538− 
2539− return hasError ? { valid: false, error: errorMessage } : { valid: true }
2540−}
2541−```
2542− 
2543−**Correct: returns immediately on first error**
2544− 
2545−```typescript
2546−function validateUsers(users: User[]) {
2547− for (const user of users) {
2548− if (!user.email) {
2549− return { valid: false, error: "Email required" }
2550− }
2551− if (!user.name) {
2552− return { valid: false, error: "Name required" }
2553− }
2554− }
2555− 
2556− return { valid: true }
2557−}
2558−```
2559− 
2560−### 7.9 Hoist RegExp Creation
2561− 
2562−**Impact: LOW-MEDIUM (avoids recreation)**
2563− 
2564−Don't create RegExp inside render. Hoist to module scope or memoize with `useMemo()`.
2565− 
2566−**Incorrect: new RegExp every render**
2567− 
2568−```tsx
2569−function Highlighter({ text, query }: Props) {
2570− const regex = new RegExp(`(${query})`, 'gi')
2571− const parts = text.split(regex)
2572− return <>{parts.map((part, i) => ...)}</>
2573−}
2574−```
2575− 
2576−**Correct: memoize or hoist**
2577− 
2578−```tsx
2579−const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
2580− 
2581−function Highlighter({ text, query }: Props) {
2582− const regex = useMemo(
2583− () => new RegExp(`(${escapeRegex(query)})`, 'gi'),
2584− [query]
2585− )
2586− const parts = text.split(regex)
2587− return <>{parts.map((part, i) => ...)}</>
2588−}
2589−```
2590− 
2591−**Warning: global regex has mutable state**
2592− 
2593−```typescript
2594−const regex = /foo/g
2595−regex.test("foo") // true, lastIndex = 3
2596−regex.test("foo") // false, lastIndex = 0
2597−```
2598− 
2599−Global regex (`/g`) has mutable `lastIndex` state:
2600− 
2601−### 7.10 Use Loop for Min/Max Instead of Sort
2602− 
2603−**Impact: LOW (O(n) instead of O(n log n))**
2604− 
2605−Finding the smallest or largest element only requires a single pass through the array. Sorting is wasteful and slower.
2606− 
2607−**Incorrect (O(n log n) - sort to find latest):**
2608− 
2609−```typescript
2610−interface Project {
2611− id: string
2612− name: string
2613− updatedAt: number
2614−}
2615− 
2616−function getLatestProject(projects: Project[]) {
2617− const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt)
2618− return sorted[0]
2619−}
2620−```
2621− 
2622−Sorts the entire array just to find the maximum value.
2623− 
2624−**Incorrect (O(n log n) - sort for oldest and newest):**
2625− 
2626−```typescript
2627−function getOldestAndNewest(projects: Project[]) {
2628− const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt)
2629− return { oldest: sorted[0], newest: sorted[sorted.length - 1] }
2630−}
2631−```
2632− 
2633−Still sorts unnecessarily when only min/max are needed.
2634− 
2635−**Correct (O(n) - single loop):**
2636− 
2637−```typescript
2638−function getLatestProject(projects: Project[]) {
2639− if (projects.length === 0) return null
2640− 
2641− let latest = projects[0]
2642− 
2643− for (let i = 1; i < projects.length; i++) {
2644− if (projects[i].updatedAt > latest.updatedAt) {
2645− latest = projects[i]
2646− }
2647− }
2648− 
2649− return latest
2650−}
2651− 
2652−function getOldestAndNewest(projects: Project[]) {
2653− if (projects.length === 0) return { oldest: null, newest: null }
2654− 
2655− let oldest = projects[0]
2656− let newest = projects[0]
2657− 
2658− for (let i = 1; i < projects.length; i++) {
2659− if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i]
2660− if (projects[i].updatedAt > newest.updatedAt) newest = projects[i]
2661− }
2662− 
2663− return { oldest, newest }
2664−}
2665−```
2666− 
2667−Single pass through the array, no copying, no sorting.
2668− 
2669−**Alternative: Math.min/Math.max for small arrays**
2670− 
2671−```typescript
2672−const numbers = [5, 2, 8, 1, 9]
2673−const min = Math.min(...numbers)
2674−const max = Math.max(...numbers)
2675−```
2676− 
2677−This works for small arrays, but can be slower or just throw an error for very large arrays due to spread operator limitations. Maximal array length is approximately 124000 in Chrome 143 and 638000 in Safari 18; exact numbers may vary - see [the fiddle](https://jsfiddle.net/qw1jabsx/4/). Use the loop approach for reliability.
2678− 
2679−### 7.11 Use Set/Map for O(1) Lookups
2680− 
2681−**Impact: LOW-MEDIUM (O(n) to O(1))**
2682− 
2683−Convert arrays to Set/Map for repeated membership checks.
2684− 
2685−**Incorrect (O(n) per check):**
2686− 
2687−```typescript
2688−const allowedIds = ['a', 'b', 'c', ...]
2689−items.filter(item => allowedIds.includes(item.id))
2690−```
2691− 
2692−**Correct (O(1) per check):**
2693− 
2694−```typescript
2695−const allowedIds = new Set(['a', 'b', 'c', ...])
2696−items.filter(item => allowedIds.has(item.id))
2697−```
2698− 
2699−### 7.12 Use toSorted() Instead of sort() for Immutability
2700− 
2701−**Impact: MEDIUM-HIGH (prevents mutation bugs in React state)**
2702− 
2703−`.sort()` mutates the array in place, which can cause bugs with React state and props. Use `.toSorted()` to create a new sorted array without mutation.
2704− 
2705−**Incorrect: mutates original array**
2706− 
2707−```typescript
2708−function UserList({ users }: { users: User[] }) {
2709− // Mutates the users prop array!
2710− const sorted = useMemo(
2711− () => users.sort((a, b) => a.name.localeCompare(b.name)),
2712− [users]
2713− )
2714− return <div>{sorted.map(renderUser)}</div>
2715−}
2716−```
2717− 
2718−**Correct: creates new array**
2719− 
2720−```typescript
2721−function UserList({ users }: { users: User[] }) {
2722− // Creates new sorted array, original unchanged
2723− const sorted = useMemo(
2724− () => users.toSorted((a, b) => a.name.localeCompare(b.name)),
2725− [users]
2726− )
2727− return <div>{sorted.map(renderUser)}</div>
2728−}
2729−```
2730− 
2731−**Why this matters in React:**
2732− 
2733−1. Props/state mutations break React's immutability model - React expects props and state to be treated as read-only
2734− 
2735−2. Causes stale closure bugs - Mutating arrays inside closures (callbacks, effects) can lead to unexpected behavior
2736− 
2737−**Browser support: fallback for older browsers**
2738− 
2739−```typescript
2740−// Fallback for older browsers
2741−const sorted = [...items].sort((a, b) => a.value - b.value)
2742−```
2743− 
2744−`.toSorted()` is available in all modern browsers (Chrome 110+, Safari 16+, Firefox 115+, Node.js 20+). For older environments, use spread operator:
2745− 
2746−**Other immutable array methods:**
2747− 
2748−- `.toSorted()` - immutable sort
2749− 
2750−- `.toReversed()` - immutable reverse
2751− 
2752−- `.toSpliced()` - immutable splice
2753− 
2754−- `.with()` - immutable element replacement
2755− 
2756−---
2757− 
2758−## 8. Advanced Patterns
2759− 
2760−**Impact: LOW**
2761− 
2762−Advanced patterns for specific cases that require careful implementation.
2763− 
2764−### 8.1 Initialize App Once, Not Per Mount
2765− 
2766−**Impact: LOW-MEDIUM (avoids duplicate init in development)**
2767− 
2768−Do not put app-wide initialization that must run once per app load inside `useEffect([])` of a component. Components can remount and effects will re-run. Use a module-level guard or top-level init in the entry module instead.
2769− 
2770−**Incorrect: runs twice in dev, re-runs on remount**
2771− 
2772−```tsx
2773−function Comp() {
2774− useEffect(() => {
2775− loadFromStorage()
2776− checkAuthToken()
2777− }, [])
2778− 
2779− // ...
2780−}
2781−```
2782− 
2783−**Correct: once per app load**
2784− 
2785−```tsx
2786−let didInit = false
2787− 
2788−function Comp() {
2789− useEffect(() => {
2790− if (didInit) return
2791− didInit = true
2792− loadFromStorage()
2793− checkAuthToken()
2794− }, [])
2795− 
2796− // ...
2797−}
2798−```
2799− 
2800−Reference: [https://react.dev/learn/you-might-not-need-an-effect#initializing-the-application](https://react.dev/learn/you-might-not-need-an-effect#initializing-the-application)
2801− 
2802−### 8.2 Store Event Handlers in Refs
2803− 
2804−**Impact: LOW (stable subscriptions)**
2805− 
2806−Store callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.
2807− 
2808−**Incorrect: re-subscribes on every render**
2809− 
2810−```tsx
2811−function useWindowEvent(event: string, handler: (e) => void) {
2812− useEffect(() => {
2813− window.addEventListener(event, handler)
2814− return () => window.removeEventListener(event, handler)
2815− }, [event, handler])
2816−}
2817−```
2818− 
2819−**Correct: stable subscription**
2820− 
2821−```tsx
2822−import { useEffectEvent } from "react"
2823− 
2824−function useWindowEvent(event: string, handler: (e) => void) {
2825− const onEvent = useEffectEvent(handler)
2826− 
2827− useEffect(() => {
2828− window.addEventListener(event, onEvent)
2829− return () => window.removeEventListener(event, onEvent)
2830− }, [event])
2831−}
2832−```
2833− 
2834−**Alternative: use `useEffectEvent` if you're on latest React:**
2835− 
2836−`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.
2837− 
2838−### 8.3 useEffectEvent for Stable Callback Refs
2839− 
2840−**Impact: LOW (prevents effect re-runs)**
2841− 
2842−Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.
2843− 
2844−**Incorrect: effect re-runs on every callback change**
2845− 
2846−```tsx
2847−function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
2848− const [query, setQuery] = useState("")
2849− 
2850− useEffect(() => {
2851− const timeout = setTimeout(() => onSearch(query), 300)
2852− return () => clearTimeout(timeout)
2853− }, [query, onSearch])
2854−}
2855−```
2856− 
2857−**Correct: using React's useEffectEvent**
2858− 
2859−```tsx
2860−import { useEffectEvent } from "react"
2861− 
2862−function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
2863− const [query, setQuery] = useState("")
2864− const onSearchEvent = useEffectEvent(onSearch)
2865− 
2866− useEffect(() => {
2867− const timeout = setTimeout(() => onSearchEvent(query), 300)
2868− return () => clearTimeout(timeout)
2869− }, [query])
2870−}
2871−```
2872− 
2873−---
2874− 
2875−## References
2876− 
2877−1. [https://react.dev](https://react.dev)
2878−2. [https://nextjs.org](https://nextjs.org)
2879−3. [https://swr.vercel.app](https://swr.vercel.app)
2880−4. [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
2881−5. [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)
2882−6. [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)
2883−7. [https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast)
1+# React Composition Patterns
2+ 
3+**Version 1.0.0**
4+Engineering
5+January 2026
6+ 
7+> **Note:**
8+> This document is mainly for agents and LLMs to follow when maintaining,
9+> generating, or refactoring React codebases using composition. Humans
10+> may also find it useful, but guidance here is optimized for automation
11+> and consistency by AI-assisted workflows.
12+ 
13+---
14+ 
15+## Abstract
16+ 
17+Composition patterns for building flexible, maintainable React components. Avoid boolean prop proliferation by using compound components, lifting state, and composing internals. These patterns make codebases easier for both humans and AI agents to work with as they scale.
18+ 
19+---
20+ 
21+## Table of Contents
22+ 
23+1. [Component Architecture](#1-component-architecture) — **HIGH**
24+ - 1.1 [Avoid Boolean Prop Proliferation](#11-avoid-boolean-prop-proliferation)
25+ - 1.2 [Use Compound Components](#12-use-compound-components)
26+2. [State Management](#2-state-management) — **MEDIUM**
27+ - 2.1 [Decouple State Management from UI](#21-decouple-state-management-from-ui)
28+ - 2.2 [Define Generic Context Interfaces for Dependency Injection](#22-define-generic-context-interfaces-for-dependency-injection)
29+ - 2.3 [Lift State into Provider Components](#23-lift-state-into-provider-components)
30+3. [Implementation Patterns](#3-implementation-patterns) — **MEDIUM**
31+ - 3.1 [Create Explicit Component Variants](#31-create-explicit-component-variants)
32+ - 3.2 [Prefer Composing Children Over Render Props](#32-prefer-composing-children-over-render-props)
33+4. [React 19 APIs](#4-react-19-apis) — **MEDIUM**
34+ - 4.1 [React 19 API Changes](#41-react-19-api-changes)
35+ 
36+---
37+ 
38+## 1. Component Architecture
39+ 
40+**Impact: HIGH**
41+ 
42+Fundamental patterns for structuring components to avoid prop
43+proliferation and enable flexible composition.
44+ 
45+### 1.1 Avoid Boolean Prop Proliferation
46+ 
47+**Impact: CRITICAL (prevents unmaintainable component variants)**
48+ 
49+Don't add boolean props like `isThread`, `isEditing`, `isDMThread` to customize
50+ 
51+component behavior. Each boolean doubles possible states and creates
52+ 
53+unmaintainable conditional logic. Use composition instead.
54+ 
55+**Incorrect: boolean props create exponential complexity**
56+ 
57+```tsx
58+function Composer({
59+ onSubmit,
60+ isThread,
61+ channelId,
62+ isDMThread,
63+ dmId,
64+ isEditing,
65+ isForwarding,
66+}: Props) {
67+ return (
68+ <form>
69+ <Header />
70+ <Input />
71+ {isDMThread ? (
72+ <AlsoSendToDMField id={dmId} />
73+ ) : isThread ? (
74+ <AlsoSendToChannelField id={channelId} />
75+ ) : null}
76+ {isEditing ? <EditActions /> : isForwarding ? <ForwardActions /> : <DefaultActions />}
77+ <Footer onSubmit={onSubmit} />
78+ </form>
79+ )
80+}
81+```
82+ 
83+**Correct: composition eliminates conditionals**
84+ 
85+```tsx
86+// Channel composer
87+function ChannelComposer() {
88+ return (
89+ <Composer.Frame>
90+ <Composer.Header />
91+ <Composer.Input />
92+ <Composer.Footer>
93+ <Composer.Attachments />
94+ <Composer.Formatting />
95+ <Composer.Emojis />
96+ <Composer.Submit />
97+ </Composer.Footer>
98+ </Composer.Frame>
99+ )
100+}
101+ 
102+// Thread composer - adds "also send to channel" field
103+function ThreadComposer({ channelId }: { channelId: string }) {
104+ return (
105+ <Composer.Frame>
106+ <Composer.Header />
107+ <Composer.Input />
108+ <AlsoSendToChannelField id={channelId} />
109+ <Composer.Footer>
110+ <Composer.Formatting />
111+ <Composer.Emojis />
112+ <Composer.Submit />
113+ </Composer.Footer>
114+ </Composer.Frame>
115+ )
116+}
117+ 
118+// Edit composer - different footer actions
119+function EditComposer() {
120+ return (
121+ <Composer.Frame>
122+ <Composer.Input />
123+ <Composer.Footer>
124+ <Composer.Formatting />
125+ <Composer.Emojis />
126+ <Composer.CancelEdit />
127+ <Composer.SaveEdit />
128+ </Composer.Footer>
129+ </Composer.Frame>
130+ )
131+}
132+```
133+ 
134+Each variant is explicit about what it renders. We can share internals without
135+ 
136+sharing a single monolithic parent.
137+ 
138+### 1.2 Use Compound Components
139+ 
140+**Impact: HIGH (enables flexible composition without prop drilling)**
141+ 
142+Structure complex components as compound components with a shared context. Each
143+ 
144+subcomponent accesses shared state via context, not props. Consumers compose the
145+ 
146+pieces they need.
147+ 
148+**Incorrect: monolithic component with render props**
149+ 
150+```tsx
151+function Composer({
152+ renderHeader,
153+ renderFooter,
154+ renderActions,
155+ showAttachments,
156+ showFormatting,
157+ showEmojis,
158+}: Props) {
159+ return (
160+ <form>
161+ {renderHeader?.()}
162+ <Input />
163+ {showAttachments && <Attachments />}
164+ {renderFooter ? (
165+ renderFooter()
166+ ) : (
167+ <Footer>
168+ {showFormatting && <Formatting />}
169+ {showEmojis && <Emojis />}
170+ {renderActions?.()}
171+ </Footer>
172+ )}
173+ </form>
174+ )
175+}
176+```
177+ 
178+**Correct: compound components with shared context**
179+ 
180+```tsx
181+const ComposerContext = createContext<ComposerContextValue | null>(null)
182+ 
183+function ComposerProvider({ children, state, actions, meta }: ProviderProps) {
184+ return <ComposerContext value={{ state, actions, meta }}>{children}</ComposerContext>
185+}
186+ 
187+function ComposerFrame({ children }: { children: React.ReactNode }) {
188+ return <form>{children}</form>
189+}
190+ 
191+function ComposerInput() {
192+ const {
193+ state,
194+ actions: { update },
195+ meta: { inputRef },
196+ } = use(ComposerContext)
197+ return (
198+ <TextInput
199+ ref={inputRef}
200+ value={state.input}
201+ onChangeText={(text) => update((s) => ({ ...s, input: text }))}
202+ />
203+ )
204+}
205+ 
206+function ComposerSubmit() {
207+ const {
208+ actions: { submit },
209+ } = use(ComposerContext)
210+ return <Button onPress={submit}>Send</Button>
211+}
212+ 
213+// Export as compound component
214+const Composer = {
215+ Provider: ComposerProvider,
216+ Frame: ComposerFrame,
217+ Input: ComposerInput,
218+ Submit: ComposerSubmit,
219+ Header: ComposerHeader,
220+ Footer: ComposerFooter,
221+ Attachments: ComposerAttachments,
222+ Formatting: ComposerFormatting,
223+ Emojis: ComposerEmojis,
224+}
225+```
226+ 
227+**Usage:**
228+ 
229+```tsx
230+<Composer.Provider state={state} actions={actions} meta={meta}>
231+ <Composer.Frame>
232+ <Composer.Header />
233+ <Composer.Input />
234+ <Composer.Footer>
235+ <Composer.Formatting />
236+ <Composer.Submit />
237+ </Composer.Footer>
238+ </Composer.Frame>
239+</Composer.Provider>
240+```
241+ 
242+Consumers explicitly compose exactly what they need. No hidden conditionals. And the state, actions and meta are dependency-injected by a parent provider, allowing multiple usages of the same component structure.
243+ 
244+---
245+ 
246+## 2. State Management
247+ 
248+**Impact: MEDIUM**
249+ 
250+Patterns for lifting state and managing shared context across
251+composed components.
252+ 
253+### 2.1 Decouple State Management from UI
254+ 
255+**Impact: MEDIUM (enables swapping state implementations without changing UI)**
256+ 
257+The provider component should be the only place that knows how state is managed.
258+ 
259+UI components consume the context interface—they don't know if state comes from
260+ 
261+useState, Zustand, or a server sync.
262+ 
263+**Incorrect: UI coupled to state implementation**
264+ 
265+```tsx
266+function ChannelComposer({ channelId }: { channelId: string }) {
267+ // UI component knows about global state implementation
268+ const state = useGlobalChannelState(channelId)
269+ const { submit, updateInput } = useChannelSync(channelId)
270+ 
271+ return (
272+ <Composer.Frame>
273+ <Composer.Input value={state.input} onChange={(text) => sync.updateInput(text)} />
274+ <Composer.Submit onPress={() => sync.submit()} />
275+ </Composer.Frame>
276+ )
277+}
278+```
279+ 
280+**Correct: state management isolated in provider**
281+ 
282+```tsx
283+// Provider handles all state management details
284+function ChannelProvider({
285+ channelId,
286+ children,
287+}: {
288+ channelId: string
289+ children: React.ReactNode
290+}) {
291+ const { state, update, submit } = useGlobalChannel(channelId)
292+ const inputRef = useRef(null)
293+ 
294+ return (
295+ <Composer.Provider state={state} actions={{ update, submit }} meta={{ inputRef }}>
296+ {children}
297+ </Composer.Provider>
298+ )
299+}
300+ 
301+// UI component only knows about the context interface
302+function ChannelComposer() {
303+ return (
304+ <Composer.Frame>
305+ <Composer.Header />
306+ <Composer.Input />
307+ <Composer.Footer>
308+ <Composer.Submit />
309+ </Composer.Footer>
310+ </Composer.Frame>
311+ )
312+}
313+ 
314+// Usage
315+function Channel({ channelId }: { channelId: string }) {
316+ return (
317+ <ChannelProvider channelId={channelId}>
318+ <ChannelComposer />
319+ </ChannelProvider>
320+ )
321+}
322+```
323+ 
324+**Different providers, same UI:**
325+ 
326+```tsx
327+// Local state for ephemeral forms
328+function ForwardMessageProvider({ children }) {
329+ const [state, setState] = useState(initialState)
330+ const forwardMessage = useForwardMessage()
331+ 
332+ return (
333+ <Composer.Provider state={state} actions={{ update: setState, submit: forwardMessage }}>
334+ {children}
335+ </Composer.Provider>
336+ )
337+}
338+ 
339+// Global synced state for channels
340+function ChannelProvider({ channelId, children }) {
341+ const { state, update, submit } = useGlobalChannel(channelId)
342+ 
343+ return (
344+ <Composer.Provider state={state} actions={{ update, submit }}>
345+ {children}
346+ </Composer.Provider>
347+ )
348+}
349+```
350+ 
351+The same `Composer.Input` component works with both providers because it only
352+ 
353+depends on the context interface, not the implementation.
354+ 
355+### 2.2 Define Generic Context Interfaces for Dependency Injection
356+ 
357+**Impact: HIGH (enables dependency-injectable state across use-cases)**
358+ 
359+Define a **generic interface** for your component context with three parts:
360+ 
361+`state`, `actions`, and `meta`. This interface is a contract that any provider
362+ 
363+can implement—enabling the same UI components to work with completely different
364+ 
365+state implementations.
366+ 
367+**Core principle:** Lift state, compose internals, make state
368+ 
369+dependency-injectable.
370+ 
371+**Incorrect: UI coupled to specific state implementation**
372+ 
373+```tsx
374+function ComposerInput() {
375+ // Tightly coupled to a specific hook
376+ const { input, setInput } = useChannelComposerState()
377+ return <TextInput value={input} onChangeText={setInput} />
378+}
379+```
380+ 
381+**Correct: generic interface enables dependency injection**
382+ 
383+```tsx
384+// Define a GENERIC interface that any provider can implement
385+interface ComposerState {
386+ input: string
387+ attachments: Attachment[]
388+ isSubmitting: boolean
389+}
390+ 
391+interface ComposerActions {
392+ update: (updater: (state: ComposerState) => ComposerState) => void
393+ submit: () => void
394+}
395+ 
396+interface ComposerMeta {
397+ inputRef: React.RefObject<TextInput>
398+}
399+ 
400+interface ComposerContextValue {
401+ state: ComposerState
402+ actions: ComposerActions
403+ meta: ComposerMeta
404+}
405+ 
406+const ComposerContext = createContext<ComposerContextValue | null>(null)
407+```
408+ 
409+**UI components consume the interface, not the implementation:**
410+ 
411+```tsx
412+function ComposerInput() {
413+ const {
414+ state,
415+ actions: { update },
416+ meta,
417+ } = use(ComposerContext)
418+ 
419+ // This component works with ANY provider that implements the interface
420+ return (
421+ <TextInput
422+ ref={meta.inputRef}
423+ value={state.input}
424+ onChangeText={(text) => update((s) => ({ ...s, input: text }))}
425+ />
426+ )
427+}
428+```
429+ 
430+**Different providers implement the same interface:**
431+ 
432+```tsx
433+// Provider A: Local state for ephemeral forms
434+function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
435+ const [state, setState] = useState(initialState)
436+ const inputRef = useRef(null)
437+ const submit = useForwardMessage()
438+ 
439+ return (
440+ <ComposerContext
441+ value={{
442+ state,
443+ actions: { update: setState, submit },
444+ meta: { inputRef },
445+ }}
446+ >
447+ {children}
448+ </ComposerContext>
449+ )
450+}
451+ 
452+// Provider B: Global synced state for channels
453+function ChannelProvider({ channelId, children }: Props) {
454+ const { state, update, submit } = useGlobalChannel(channelId)
455+ const inputRef = useRef(null)
456+ 
457+ return (
458+ <ComposerContext
459+ value={{
460+ state,
461+ actions: { update, submit },
462+ meta: { inputRef },
463+ }}
464+ >
465+ {children}
466+ </ComposerContext>
467+ )
468+}
469+```
470+ 
471+**The same composed UI works with both:**
472+ 
473+```tsx
474+// Works with ForwardMessageProvider (local state)
475+<ForwardMessageProvider>
476+ <Composer.Frame>
477+ <Composer.Input />
478+ <Composer.Submit />
479+ </Composer.Frame>
480+</ForwardMessageProvider>
481+ 
482+// Works with ChannelProvider (global synced state)
483+<ChannelProvider channelId="abc">
484+ <Composer.Frame>
485+ <Composer.Input />
486+ <Composer.Submit />
487+ </Composer.Frame>
488+</ChannelProvider>
489+```
490+ 
491+**Custom UI outside the component can access state and actions:**
492+ 
493+```tsx
494+function ForwardMessageDialog() {
495+ return (
496+ <ForwardMessageProvider>
497+ <Dialog>
498+ {/* The composer UI */}
499+ <Composer.Frame>
500+ <Composer.Input placeholder="Add a message, if you'd like." />
501+ <Composer.Footer>
502+ <Composer.Formatting />
503+ <Composer.Emojis />
504+ </Composer.Footer>
505+ </Composer.Frame>
506+ 
507+ {/* Custom UI OUTSIDE the composer, but INSIDE the provider */}
508+ <MessagePreview />
509+ 
510+ {/* Actions at the bottom of the dialog */}
511+ <DialogActions>
512+ <CancelButton />
513+ <ForwardButton />
514+ </DialogActions>
515+ </Dialog>
516+ </ForwardMessageProvider>
517+ )
518+}
519+ 
520+// This button lives OUTSIDE Composer.Frame but can still submit based on its context!
521+function ForwardButton() {
522+ const {
523+ actions: { submit },
524+ } = use(ComposerContext)
525+ return <Button onPress={submit}>Forward</Button>
526+}
527+ 
528+// This preview lives OUTSIDE Composer.Frame but can read composer's state!
529+function MessagePreview() {
530+ const { state } = use(ComposerContext)
531+ return <Preview message={state.input} attachments={state.attachments} />
532+}
533+```
534+ 
535+The provider boundary is what matters—not the visual nesting. Components that
536+ 
537+need shared state don't have to be inside the `Composer.Frame`. They just need
538+ 
539+to be within the provider.
540+ 
541+The `ForwardButton` and `MessagePreview` are not visually inside the composer
542+ 
543+box, but they can still access its state and actions. This is the power of
544+ 
545+lifting state into providers.
546+ 
547+The UI is reusable bits you compose together. The state is dependency-injected
548+ 
549+by the provider. Swap the provider, keep the UI.
550+ 
551+### 2.3 Lift State into Provider Components
552+ 
553+**Impact: HIGH (enables state sharing outside component boundaries)**
554+ 
555+Move state management into dedicated provider components. This allows sibling
556+ 
557+components outside the main UI to access and modify state without prop drilling
558+ 
559+or awkward refs.
560+ 
561+**Incorrect: state trapped inside component**
562+ 
563+```tsx
564+function ForwardMessageComposer() {
565+ const [state, setState] = useState(initialState)
566+ const forwardMessage = useForwardMessage()
567+ 
568+ return (
569+ <Composer.Frame>
570+ <Composer.Input />
571+ <Composer.Footer />
572+ </Composer.Frame>
573+ )
574+}
575+ 
576+// Problem: How does this button access composer state?
577+function ForwardMessageDialog() {
578+ return (
579+ <Dialog>
580+ <ForwardMessageComposer />
581+ <MessagePreview /> {/* Needs composer state */}
582+ <DialogActions>
583+ <CancelButton />
584+ <ForwardButton /> {/* Needs to call submit */}
585+ </DialogActions>
586+ </Dialog>
587+ )
588+}
589+```
590+ 
591+**Incorrect: useEffect to sync state up**
592+ 
593+```tsx
594+function ForwardMessageDialog() {
595+ const [input, setInput] = useState("")
596+ return (
597+ <Dialog>
598+ <ForwardMessageComposer onInputChange={setInput} />
599+ <MessagePreview input={input} />
600+ </Dialog>
601+ )
602+}
603+ 
604+function ForwardMessageComposer({ onInputChange }) {
605+ const [state, setState] = useState(initialState)
606+ useEffect(() => {
607+ onInputChange(state.input) // Sync on every change 😬
608+ }, [state.input])
609+}
610+```
611+ 
612+**Incorrect: reading state from ref on submit**
613+ 
614+```tsx
615+function ForwardMessageDialog() {
616+ const stateRef = useRef(null)
617+ return (
618+ <Dialog>
619+ <ForwardMessageComposer stateRef={stateRef} />
620+ <ForwardButton onPress={() => submit(stateRef.current)} />
621+ </Dialog>
622+ )
623+}
624+```
625+ 
626+**Correct: state lifted to provider**
627+ 
628+```tsx
629+function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
630+ const [state, setState] = useState(initialState)
631+ const forwardMessage = useForwardMessage()
632+ const inputRef = useRef(null)
633+ 
634+ return (
635+ <Composer.Provider
636+ state={state}
637+ actions={{ update: setState, submit: forwardMessage }}
638+ meta={{ inputRef }}
639+ >
640+ {children}
641+ </Composer.Provider>
642+ )
643+}
644+ 
645+function ForwardMessageDialog() {
646+ return (
647+ <ForwardMessageProvider>
648+ <Dialog>
649+ <ForwardMessageComposer />
650+ <MessagePreview /> {/* Custom components can access state and actions */}
651+ <DialogActions>
652+ <CancelButton />
653+ <ForwardButton /> {/* Custom components can access state and actions */}
654+ </DialogActions>
655+ </Dialog>
656+ </ForwardMessageProvider>
657+ )
658+}
659+ 
660+function ForwardButton() {
661+ const { actions } = use(Composer.Context)
662+ return <Button onPress={actions.submit}>Forward</Button>
663+}
664+```
665+ 
666+The ForwardButton lives outside the Composer.Frame but still has access to the
667+ 
668+submit action because it's within the provider. Even though it's a one-off
669+ 
670+component, it can still access the composer's state and actions from outside the
671+ 
672+UI itself.
673+ 
674+**Key insight:** Components that need shared state don't have to be visually
675+ 
676+nested inside each other—they just need to be within the same provider.
677+ 
678+---
679+ 
680+## 3. Implementation Patterns
681+ 
682+**Impact: MEDIUM**
683+ 
684+Specific techniques for implementing compound components and
685+context providers.
686+ 
687+### 3.1 Create Explicit Component Variants
688+ 
689+**Impact: MEDIUM (self-documenting code, no hidden conditionals)**
690+ 
691+Instead of one component with many boolean props, create explicit variant
692+ 
693+components. Each variant composes the pieces it needs. The code documents
694+ 
695+itself.
696+ 
697+**Incorrect: one component, many modes**
698+ 
699+```tsx
700+// What does this component actually render?
701+<Composer isThread isEditing={false} channelId="abc" showAttachments showFormatting={false} />
702+```
703+ 
704+**Correct: explicit variants**
705+ 
706+```tsx
707+// Immediately clear what this renders
708+<ThreadComposer channelId="abc" />
709+ 
710+// Or
711+<EditMessageComposer messageId="xyz" />
712+ 
713+// Or
714+<ForwardMessageComposer messageId="123" />
715+```
716+ 
717+Each implementation is unique, explicit and self-contained. Yet they can each
718+ 
719+use shared parts.
720+ 
721+**Implementation:**
722+ 
723+```tsx
724+function ThreadComposer({ channelId }: { channelId: string }) {
725+ return (
726+ <ThreadProvider channelId={channelId}>
727+ <Composer.Frame>
728+ <Composer.Input />
729+ <AlsoSendToChannelField channelId={channelId} />
730+ <Composer.Footer>
731+ <Composer.Formatting />
732+ <Composer.Emojis />
733+ <Composer.Submit />
734+ </Composer.Footer>
735+ </Composer.Frame>
736+ </ThreadProvider>
737+ )
738+}
739+ 
740+function EditMessageComposer({ messageId }: { messageId: string }) {
741+ return (
742+ <EditMessageProvider messageId={messageId}>
743+ <Composer.Frame>
744+ <Composer.Input />
745+ <Composer.Footer>
746+ <Composer.Formatting />
747+ <Composer.Emojis />
748+ <Composer.CancelEdit />
749+ <Composer.SaveEdit />
750+ </Composer.Footer>
751+ </Composer.Frame>
752+ </EditMessageProvider>
753+ )
754+}
755+ 
756+function ForwardMessageComposer({ messageId }: { messageId: string }) {
757+ return (
758+ <ForwardMessageProvider messageId={messageId}>
759+ <Composer.Frame>
760+ <Composer.Input placeholder="Add a message, if you'd like." />
761+ <Composer.Footer>
762+ <Composer.Formatting />
763+ <Composer.Emojis />
764+ <Composer.Mentions />
765+ </Composer.Footer>
766+ </Composer.Frame>
767+ </ForwardMessageProvider>
768+ )
769+}
770+```
771+ 
772+Each variant is explicit about:
773+ 
774+- What provider/state it uses
775+ 
776+- What UI elements it includes
777+ 
778+- What actions are available
779+ 
780+No boolean prop combinations to reason about. No impossible states.
781+ 
782+### 3.2 Prefer Composing Children Over Render Props
783+ 
784+**Impact: MEDIUM (cleaner composition, better readability)**
785+ 
786+Use `children` for composition instead of `renderX` props. Children are more
787+ 
788+readable, compose naturally, and don't require understanding callback
789+ 
790+signatures.
791+ 
792+**Incorrect: render props**
793+ 
794+```tsx
795+function Composer({
796+ renderHeader,
797+ renderFooter,
798+ renderActions,
799+}: {
800+ renderHeader?: () => React.ReactNode
801+ renderFooter?: () => React.ReactNode
802+ renderActions?: () => React.ReactNode
803+}) {
804+ return (
805+ <form>
806+ {renderHeader?.()}
807+ <Input />
808+ {renderFooter ? renderFooter() : <DefaultFooter />}
809+ {renderActions?.()}
810+ </form>
811+ )
812+}
813+ 
814+// Usage is awkward and inflexible
815+return (
816+ <Composer
817+ renderHeader={() => <CustomHeader />}
818+ renderFooter={() => (
819+ <>
820+ <Formatting />
821+ <Emojis />
822+ </>
823+ )}
824+ renderActions={() => <SubmitButton />}
825+ />
826+)
827+```
828+ 
829+**Correct: compound components with children**
830+ 
831+```tsx
832+function ComposerFrame({ children }: { children: React.ReactNode }) {
833+ return <form>{children}</form>
834+}
835+ 
836+function ComposerFooter({ children }: { children: React.ReactNode }) {
837+ return <footer className="flex">{children}</footer>
838+}
839+ 
840+// Usage is flexible
841+return (
842+ <Composer.Frame>
843+ <CustomHeader />
844+ <Composer.Input />
845+ <Composer.Footer>
846+ <Composer.Formatting />
847+ <Composer.Emojis />
848+ <SubmitButton />
849+ </Composer.Footer>
850+ </Composer.Frame>
851+)
852+```
853+ 
854+**When render props are appropriate:**
855+ 
856+```tsx
857+// Render props work well when you need to pass data back
858+<List data={items} renderItem={({ item, index }) => <Item item={item} index={index} />} />
859+```
860+ 
861+Use render props when the parent needs to provide data or state to the child.
862+ 
863+Use children when composing static structure.
864+ 
865+---
866+ 
867+## 4. React 19 APIs
868+ 
869+**Impact: MEDIUM**
870+ 
871+React 19+ only. Don't use `forwardRef`; use `use()` instead of `useContext()`.
872+ 
873+### 4.1 React 19 API Changes
874+ 
875+**Impact: MEDIUM (cleaner component definitions and context usage)**
876+ 
877+> **⚠️ React 19+ only.** Skip this if you're on React 18 or earlier.
878+ 
879+In React 19, `ref` is now a regular prop (no `forwardRef` wrapper needed), and `use()` replaces `useContext()`.
880+ 
881+**Incorrect: forwardRef in React 19**
882+ 
883+```tsx
884+const ComposerInput = forwardRef<TextInput, Props>((props, ref) => {
885+ return <TextInput ref={ref} {...props} />
886+})
887+```
888+ 
889+**Correct: ref as a regular prop**
890+ 
891+```tsx
892+function ComposerInput({ ref, ...props }: Props & { ref?: React.Ref<TextInput> }) {
893+ return <TextInput ref={ref} {...props} />
894+}
895+```
896+ 
897+**Incorrect: useContext in React 19**
898+ 
899+```tsx
900+const value = useContext(MyContext)
901+```
902+ 
903+**Correct: use instead of useContext**
904+ 
905+```tsx
906+const value = use(MyContext)
907+```
908+ 
909+`use()` can also be called conditionally, unlike `useContext()`.
910+ 
911+---
912+ 
913+## References
914+ 
915+1. [https://react.dev](https://react.dev)
916+2. [https://react.dev/learn/passing-data-deeply-with-context](https://react.dev/learn/passing-data-deeply-with-context)
917+3. [https://react.dev/reference/react/use](https://react.dev/reference/react/use)
2884918  
RuleStack

Built by

Kynth Studio

Directory

Configs
Stacks
Compare formats
Diff two configs
Best AGENTS.md examples

Formats

AGENTS.md
CLAUDE.md
Cursor rules
Copilot instructions

Reference

Read API
Corpus health
Privacy Policy
Terms

RuleStack

RuleStack

Built by

Kynth Studio

Directory

Configs
Stacks
Compare formats
Diff two configs
Best AGENTS.md examples

Formats

AGENTS.md
CLAUDE.md
Cursor rules
Copilot instructions

Reference

Read API
Corpus health
Privacy Policy
Terms

RuleStack

RuleStack

Built by

Kynth Studio

Directory

Configs
Stacks
Compare formats
Diff two configs
Best AGENTS.md examples

Formats

AGENTS.md
CLAUDE.md
Cursor rules
Copilot instructions

Reference

Read API
Corpus health
Privacy Policy
Terms

RuleStack