

Also from Kynth Studios


Also from Kynth Studios


Also from Kynth Studios
1<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:_sections:start -->2# Sections34This file defines all sections, their ordering, impact levels, and descriptions.5The section ID (in parentheses) is the filename prefix used to group rules.67---89## 1. Eliminating Waterfalls (async)1011**Impact:** CRITICAL12**Description:** Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains.1314## 2. Bundle Size Optimization (bundle)1516**Impact:** CRITICAL17**Description:** Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint.1819## 3. Server-Side Performance (server)2021**Impact:** HIGH22**Description:** Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times.2324## 4. Client-Side Data Fetching (client)2526**Impact:** MEDIUM-HIGH27**Description:** Automatic deduplication and efficient data fetching patterns reduce redundant network requests.2829## 5. Re-render Optimization (rerender)3031**Impact:** MEDIUM32**Description:** Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness.3334## 6. Rendering Performance (rendering)3536**Impact:** MEDIUM37**Description:** Optimizing the rendering process reduces the work the browser needs to do.3839## 7. JavaScript Performance (js)4041**Impact:** LOW-MEDIUM42**Description:** Micro-optimizations for hot paths can add up to meaningful improvements.4344## 8. Advanced Patterns (advanced)4546**Impact:** LOW47**Description:** Advanced patterns for specific cases that require careful implementation.48<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:_sections:end -->4950<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:_template:start -->51## Rule Title Here5253**Impact: MEDIUM (optional impact description)**5455Brief explanation of the rule and why it matters. This should be clear and concise, explaining the performance implications.5657**Incorrect (description of what's wrong):**5859```typescript60// Bad code example here61const bad = example()62```6364**Correct (description of what's right):**6566```typescript67// Good code example here68const good = example()69```7071Reference: [Link to documentation or resource](https://example.com)72<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:_template:end -->7374<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:advanced-effect-event-deps:start -->75## Do Not Put Effect Events in Dependency Arrays7677Effect Event functions do not have a stable identity. Their identity intentionally changes on every render. Do not include the function returned by `useEffectEvent` in a `useEffect` dependency array. Keep the actual reactive values as dependencies and call the Effect Event from inside the effect body or subscriptions created by that effect.7879**Incorrect (Effect Event added as a dependency):**8081```tsx82import { useEffect, useEffectEvent } from 'react'8384function ChatRoom({ roomId, onConnected }: {85 roomId: string86 onConnected: () => void87}) {88 const handleConnected = useEffectEvent(onConnected)8990 useEffect(() => {91 const connection = createConnection(roomId)92 connection.on('connected', handleConnected)93 connection.connect()9495 return () => connection.disconnect()96 }, [roomId, handleConnected])97}98```99100Including the Effect Event in dependencies makes the effect re-run every render and triggers the React Hooks lint rule.101102**Correct (depend on reactive values, not the Effect Event):**103104```tsx105import { useEffect, useEffectEvent } from 'react'106107function ChatRoom({ roomId, onConnected }: {108 roomId: string109 onConnected: () => void110}) {111 const handleConnected = useEffectEvent(onConnected)112113 useEffect(() => {114 const connection = createConnection(roomId)115 connection.on('connected', handleConnected)116 connection.connect()117118 return () => connection.disconnect()119 }, [roomId])120}121```122123Reference: [React useEffectEvent: Effect Event in deps](https://react.dev/reference/react/useEffectEvent#effect-event-in-deps)124<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:advanced-effect-event-deps:end -->125126<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:advanced-event-handler-refs:start -->127## Store Event Handlers in Refs128129Store callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.130131**Incorrect (re-subscribes on every render):**132133```tsx134function useWindowEvent(event: string, handler: (e) => void) {135 useEffect(() => {136 window.addEventListener(event, handler)137 return () => window.removeEventListener(event, handler)138 }, [event, handler])139}140```141142**Correct (stable subscription):**143144```tsx145function useWindowEvent(event: string, handler: (e) => void) {146 const handlerRef = useRef(handler)147 useEffect(() => {148 handlerRef.current = handler149 }, [handler])150151 useEffect(() => {152 const listener = (e) => handlerRef.current(e)153 window.addEventListener(event, listener)154 return () => window.removeEventListener(event, listener)155 }, [event])156}157```158159**Alternative: use `useEffectEvent` if you're on latest React:**160161```tsx162import { useEffectEvent } from 'react'163164function useWindowEvent(event: string, handler: (e) => void) {165 const onEvent = useEffectEvent(handler)166167 useEffect(() => {168 window.addEventListener(event, onEvent)169 return () => window.removeEventListener(event, onEvent)170 }, [event])171}172```173174`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.175<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:advanced-event-handler-refs:end -->176177<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:advanced-init-once:start -->178## Initialize App Once, Not Per Mount179180Do 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.181182**Incorrect (runs twice in dev, re-runs on remount):**183184```tsx185function Comp() {186 useEffect(() => {187 loadFromStorage()188 checkAuthToken()189 }, [])190191 // ...192}193```194195**Correct (once per app load):**196197```tsx198let didInit = false199200function Comp() {201 useEffect(() => {202 if (didInit) return203 didInit = true204 loadFromStorage()205 checkAuthToken()206 }, [])207208 // ...209}210```211212Reference: [Initializing the application](https://react.dev/learn/you-might-not-need-an-effect#initializing-the-application)213<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:advanced-init-once:end -->214215<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:advanced-use-latest:start -->216## useEffectEvent for Stable Callback Refs217218Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.219220**Incorrect (effect re-runs on every callback change):**221222```tsx223function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {224 const [query, setQuery] = useState('')225226 useEffect(() => {227 const timeout = setTimeout(() => onSearch(query), 300)228 return () => clearTimeout(timeout)229 }, [query, onSearch])230}231```232233**Correct (using React's useEffectEvent):**234235```tsx236import { useEffectEvent } from 'react';237238function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {239 const [query, setQuery] = useState('')240 const onSearchEvent = useEffectEvent(onSearch)241242 useEffect(() => {243 const timeout = setTimeout(() => onSearchEvent(query), 300)244 return () => clearTimeout(timeout)245 }, [query])246}247```248<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:advanced-use-latest:end -->249250<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:async-api-routes:start -->251## Prevent Waterfall Chains in API Routes252253In API routes and Server Actions, start independent operations immediately, even if you don't await them yet.254255**Incorrect (config waits for auth, data waits for both):**256257```typescript258export async function GET(request: Request) {259 const session = await auth()260 const config = await fetchConfig()261 const data = await fetchData(session.user.id)262 return Response.json({ data, config })263}264```265266**Correct (auth and config start immediately):**267268```typescript269export async function GET(request: Request) {270 const sessionPromise = auth()271 const configPromise = fetchConfig()272 const session = await sessionPromise273 const [config, data] = await Promise.all([274 configPromise,275 fetchData(session.user.id)276 ])277 return Response.json({ data, config })278}279```280281For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization).282<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:async-api-routes:end -->283284<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:async-cheap-condition-before-await:start -->285## Check Cheap Conditions Before Async Flags286287When a branch uses `await` for a flag or remote value and also requires a **cheap synchronous** condition (local props, request metadata, already-loaded state), evaluate the cheap condition **first**. Otherwise you pay for the async call even when the compound condition can never be true.288289This is a specialization of [Defer Await Until Needed](./async-defer-await.md) for `flag && cheapCondition` style checks.290291**Incorrect:**292293```typescript294const someFlag = await getFlag()295296if (someFlag && someCondition) {297 // ...298}299```300301**Correct:**302303```typescript304if (someCondition) {305 const someFlag = await getFlag()306 if (someFlag) {307 // ...308 }309}310```311312This matters when `getFlag` hits the network, a feature-flag service, or `React.cache` / DB work: skipping it when `someCondition` is false removes that cost on the cold path.313314Keep the original order if `someCondition` is expensive, depends on the flag, or you must run side effects in a fixed order.315<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:async-cheap-condition-before-await:end -->316317<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:async-defer-await:start -->318## Defer Await Until Needed319320Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them.321322**Incorrect (blocks both branches):**323324```typescript325async function handleRequest(userId: string, skipProcessing: boolean) {326 const userData = await fetchUserData(userId)327328 if (skipProcessing) {329 // Returns immediately but still waited for userData330 return { skipped: true }331 }332333 // Only this branch uses userData334 return processUserData(userData)335}336```337338**Correct (only blocks when needed):**339340```typescript341async function handleRequest(userId: string, skipProcessing: boolean) {342 if (skipProcessing) {343 // Returns immediately without waiting344 return { skipped: true }345 }346347 // Fetch only when needed348 const userData = await fetchUserData(userId)349 return processUserData(userData)350}351```352353**Another example (early return optimization):**354355```typescript356// Incorrect: always fetches permissions357async function updateResource(resourceId: string, userId: string) {358 const permissions = await fetchPermissions(userId)359 const resource = await getResource(resourceId)360361 if (!resource) {362 return { error: 'Not found' }363 }364365 if (!permissions.canEdit) {366 return { error: 'Forbidden' }367 }368369 return await updateResourceData(resource, permissions)370}371372// Correct: fetches only when needed373async function updateResource(resourceId: string, userId: string) {374 const resource = await getResource(resourceId)375376 if (!resource) {377 return { error: 'Not found' }378 }379380 const permissions = await fetchPermissions(userId)381382 if (!permissions.canEdit) {383 return { error: 'Forbidden' }384 }385386 return await updateResourceData(resource, permissions)387}388```389390This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive.391392For `await getFlag()` combined with a cheap synchronous guard (`flag && someCondition`), see [Check Cheap Conditions Before Async Flags](./async-cheap-condition-before-await.md).393<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:async-defer-await:end -->394395<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:async-dependencies:start -->396## Dependency-Based Parallelization397398For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment.399400**Incorrect (profile waits for config unnecessarily):**401402```typescript403const [user, config] = await Promise.all([404 fetchUser(),405 fetchConfig()406])407const profile = await fetchProfile(user.id)408```409410**Correct (config and profile run in parallel):**411412```typescript413import { all } from 'better-all'414415const { user, config, profile } = await all({416 async user() { return fetchUser() },417 async config() { return fetchConfig() },418 async profile() {419 return fetchProfile((await this.$.user).id)420 }421})422```423424**Alternative without extra dependencies:**425426We can also create all the promises first, and do `Promise.all()` at the end.427428```typescript429const userPromise = fetchUser()430const profilePromise = userPromise.then(user => fetchProfile(user.id))431432const [user, config, profile] = await Promise.all([433 userPromise,434 fetchConfig(),435 profilePromise436])437```438439Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)440<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:async-dependencies:end -->441442<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:async-parallel:start -->443## Promise.all() for Independent Operations444445When async operations have no interdependencies, execute them concurrently using `Promise.all()`.446447**Incorrect (sequential execution, 3 round trips):**448449```typescript450const user = await fetchUser()451const posts = await fetchPosts()452const comments = await fetchComments()453```454455**Correct (parallel execution, 1 round trip):**456457```typescript458const [user, posts, comments] = await Promise.all([459 fetchUser(),460 fetchPosts(),461 fetchComments()462])463```464<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:async-parallel:end -->465466<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:async-suspense-boundaries:start -->467## Strategic Suspense Boundaries468469Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads.470471**Incorrect (wrapper blocked by data fetching):**472473```tsx474async function Page() {475 const data = await fetchData() // Blocks entire page476477 return (478 <div>479 <div>Sidebar</div>480 <div>Header</div>481 <div>482 <DataDisplay data={data} />483 </div>484 <div>Footer</div>485 </div>486 )487}488```489490The entire layout waits for data even though only the middle section needs it.491492**Correct (wrapper shows immediately, data streams in):**493494```tsx495function Page() {496 return (497 <div>498 <div>Sidebar</div>499 <div>Header</div>500 <div>501 <Suspense fallback={<Skeleton />}>502 <DataDisplay />503 </Suspense>504 </div>505 <div>Footer</div>506 </div>507 )508}509510async function DataDisplay() {511 const data = await fetchData() // Only blocks this component512 return <div>{data.content}</div>513}514```515516Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data.517518**Alternative (share promise across components):**519520```tsx521function Page() {522 // Start fetch immediately, but don't await523 const dataPromise = fetchData()524525 return (526 <div>527 <div>Sidebar</div>528 <div>Header</div>529 <Suspense fallback={<Skeleton />}>530 <DataDisplay dataPromise={dataPromise} />531 <DataSummary dataPromise={dataPromise} />532 </Suspense>533 <div>Footer</div>534 </div>535 )536}537538function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {539 const data = use(dataPromise) // Unwraps the promise540 return <div>{data.content}</div>541}542543function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {544 const data = use(dataPromise) // Reuses the same promise545 return <div>{data.summary}</div>546}547```548549Both components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together.550551**When NOT to use this pattern:**552553- Critical data needed for layout decisions (affects positioning)554- SEO-critical content above the fold555- Small, fast queries where suspense overhead isn't worth it556- When you want to avoid layout shift (loading → content jump)557558**Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities.559<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:async-suspense-boundaries:end -->560561<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:bundle-analyzable-paths:start -->562## Prefer Statically Analyzable Paths563564Build tools work best when import and file-system paths are obvious at build time. If you hide the real path inside a variable or compose it too dynamically, the tool either has to include a broad set of possible files, warn that it cannot analyze the import, or widen file tracing to stay safe.565566Prefer explicit maps or literal paths so the set of reachable files stays narrow and predictable. This is the same rule whether you are choosing modules with `import()` or reading files in server/build code.567568When analysis becomes too broad, the cost is real:569- Larger server bundles570- Slower builds571- Worse cold starts572- More memory use573574### Import Paths575576**Incorrect (the bundler cannot tell what may be imported):**577578```ts579const PAGE_MODULES = {580 home: './pages/home',581 settings: './pages/settings',582} as const583584const Page = await import(PAGE_MODULES[pageName])585```586587**Correct (use an explicit map of allowed modules):**588589```ts590const PAGE_MODULES = {591 home: () => import('./pages/home'),592 settings: () => import('./pages/settings'),593} as const594595const Page = await PAGE_MODULES[pageName]()596```597598### File-System Paths599600**Incorrect (a 2-value enum still hides the final path from static analysis):**601602```ts603const baseDir = path.join(process.cwd(), 'content/' + contentKind)604```605606**Correct (make each final path literal at the callsite):**607608```ts609const baseDir =610 kind === ContentKind.Blog611 ? path.join(process.cwd(), 'content/blog')612 : path.join(process.cwd(), 'content/docs')613```614615In Next.js server code, this matters for output file tracing too. `path.join(process.cwd(), someVar)` can widen the traced file set because Next.js statically analyze `import`, `require`, and `fs` usage.616617Reference: [Next.js output](https://nextjs.org/docs/app/api-reference/config/next-config-js/output), [Next.js dynamic imports](https://nextjs.org/learn/seo/dynamic-imports), [Vite features](https://vite.dev/guide/features.html), [esbuild API](https://esbuild.github.io/api/), [Rollup dynamic import vars](https://www.npmjs.com/package/@rollup/plugin-dynamic-import-vars), [Webpack dependency management](https://webpack.js.org/guides/dependency-management/)618<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:bundle-analyzable-paths:end -->619620<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:bundle-barrel-imports:start -->621## Avoid Barrel File Imports622623Import 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'`).624625Popular 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.626627**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.628629**Incorrect (imports entire library):**630631```tsx632import { Check, X, Menu } from 'lucide-react'633// Loads 1,583 modules, takes ~2.8s extra in dev634// Runtime cost: 200-800ms on every cold start635636import { Button, TextField } from '@mui/material'637// Loads 2,225 modules, takes ~4.2s extra in dev638```639640**Correct - Next.js 13.5+ (recommended):**641642```js643// next.config.js - automatically optimizes barrel imports at build time644module.exports = {645 experimental: {646 optimizePackageImports: ['lucide-react', '@mui/material']647 }648}649```650651```tsx652// Keep the standard imports - Next.js transforms them to direct imports653import { Check, X, Menu } from 'lucide-react'654// Full TypeScript support, no manual path wrangling655```656657This is the recommended approach because it preserves TypeScript type safety and editor autocompletion while still eliminating the barrel import cost.658659**Correct - Direct imports (non-Next.js projects):**660661```tsx662import Button from '@mui/material/Button'663import TextField from '@mui/material/TextField'664// Loads only what you use665```666667> **TypeScript warning:** Some libraries (notably `lucide-react`) don't ship `.d.ts` files for their deep import paths. Importing from `lucide-react/dist/esm/icons/check` resolves to an implicit `any` type, causing errors under `strict` or `noImplicitAny`. Prefer `optimizePackageImports` when available, or verify the library exports types for its subpaths before using direct imports.668669These optimizations provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR.670671Libraries 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`.672673Reference: [How we optimized package imports in Next.js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)674<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:bundle-barrel-imports:end -->675676<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:bundle-conditional:start -->677## Conditional Module Loading678679Load large data or modules only when a feature is activated.680681**Example (lazy-load animation frames):**682683```tsx684function AnimationPlayer({ enabled, setEnabled }: { enabled: boolean; setEnabled: React.Dispatch<React.SetStateAction<boolean>> }) {685 const [frames, setFrames] = useState<Frame[] | null>(null)686687 useEffect(() => {688 if (enabled && !frames && typeof window !== 'undefined') {689 import('./animation-frames.js')690 .then(mod => setFrames(mod.frames))691 .catch(() => setEnabled(false))692 }693 }, [enabled, frames, setEnabled])694695 if (!frames) return <Skeleton />696 return <Canvas frames={frames} />697}698```699700The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed.701<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:bundle-conditional:end -->702703<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:bundle-defer-third-party:start -->704## Defer Non-Critical Third-Party Libraries705706Analytics, logging, and error tracking don't block user interaction. Load them after hydration.707708**Incorrect (blocks initial bundle):**709710```tsx711import { Analytics } from '@vercel/analytics/react'712713export default function RootLayout({ children }) {714 return (715 <html>716 <body>717 {children}718 <Analytics />719 </body>720 </html>721 )722}723```724725**Correct (loads after hydration):**726727```tsx728import dynamic from 'next/dynamic'729730const Analytics = dynamic(731 () => import('@vercel/analytics/react').then(m => m.Analytics),732 { ssr: false }733)734735export default function RootLayout({ children }) {736 return (737 <html>738 <body>739 {children}740 <Analytics />741 </body>742 </html>743 )744}745```746<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:bundle-defer-third-party:end -->747748<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:bundle-dynamic-imports:start -->749## Dynamic Imports for Heavy Components750751Use `next/dynamic` to lazy-load large components not needed on initial render.752753**Incorrect (Monaco bundles with main chunk ~300KB):**754755```tsx756import { MonacoEditor } from './monaco-editor'757758function CodePanel({ code }: { code: string }) {759 return <MonacoEditor value={code} />760}761```762763**Correct (Monaco loads on demand):**764765```tsx766import dynamic from 'next/dynamic'767768const MonacoEditor = dynamic(769 () => import('./monaco-editor').then(m => m.MonacoEditor),770 { ssr: false }771)772773function CodePanel({ code }: { code: string }) {774 return <MonacoEditor value={code} />775}776```777<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:bundle-dynamic-imports:end -->778779<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:bundle-preload:start -->780## Preload Based on User Intent781782Preload heavy bundles before they're needed to reduce perceived latency.783784**Example (preload on hover/focus):**785786```tsx787function EditorButton({ onClick }: { onClick: () => void }) {788 const preload = () => {789 if (typeof window !== 'undefined') {790 void import('./monaco-editor')791 }792 }793794 return (795 <button796 onMouseEnter={preload}797 onFocus={preload}798 onClick={onClick}799 >800 Open Editor801 </button>802 )803}804```805806**Example (preload when feature flag is enabled):**807808```tsx809function FlagsProvider({ children, flags }: Props) {810 useEffect(() => {811 if (flags.editorEnabled && typeof window !== 'undefined') {812 void import('./monaco-editor').then(mod => mod.init())813 }814 }, [flags.editorEnabled])815816 return <FlagsContext.Provider value={flags}>817 {children}818 </FlagsContext.Provider>819}820```821822The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed.823<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:bundle-preload:end -->824825<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:client-event-listeners:start -->826## Deduplicate Global Event Listeners827828Use `useSWRSubscription()` to share global event listeners across component instances.829830**Incorrect (N instances = N listeners):**831832```tsx833function useKeyboardShortcut(key: string, callback: () => void) {834 useEffect(() => {835 const handler = (e: KeyboardEvent) => {836 if (e.metaKey && e.key === key) {837 callback()838 }839 }840 window.addEventListener('keydown', handler)841 return () => window.removeEventListener('keydown', handler)842 }, [key, callback])843}844```845846When using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener.847848**Correct (N instances = 1 listener):**849850```tsx851import useSWRSubscription from 'swr/subscription'852853// Module-level Map to track callbacks per key854const keyCallbacks = new Map<string, Set<() => void>>()855856function useKeyboardShortcut(key: string, callback: () => void) {857 // Register this callback in the Map858 useEffect(() => {859 if (!keyCallbacks.has(key)) {860 keyCallbacks.set(key, new Set())861 }862 keyCallbacks.get(key)!.add(callback)863864 return () => {865 const set = keyCallbacks.get(key)866 if (set) {867 set.delete(callback)868 if (set.size === 0) {869 keyCallbacks.delete(key)870 }871 }872 }873 }, [key, callback])874875 useSWRSubscription('global-keydown', () => {876 const handler = (e: KeyboardEvent) => {877 if (e.metaKey && keyCallbacks.has(e.key)) {878 keyCallbacks.get(e.key)!.forEach(cb => cb())879 }880 }881 window.addEventListener('keydown', handler)882 return () => window.removeEventListener('keydown', handler)883 })884}885886function Profile() {887 // Multiple shortcuts will share the same listener888 useKeyboardShortcut('p', () => { /* ... */ })889 useKeyboardShortcut('k', () => { /* ... */ })890 // ...891}892```893<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:client-event-listeners:end -->894895<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:client-localstorage-schema:start -->896## Version and Minimize localStorage Data897898Add version prefix to keys and store only needed fields. Prevents schema conflicts and accidental storage of sensitive data.899900**Incorrect:**901902```typescript903// No version, stores everything, no error handling904localStorage.setItem('userConfig', JSON.stringify(fullUserObject))905const data = localStorage.getItem('userConfig')906```907908**Correct:**909910```typescript911const VERSION = 'v2'912913function saveConfig(config: { theme: string; language: string }) {914 try {915 localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config))916 } catch {917 // Throws in incognito/private browsing, quota exceeded, or disabled918 }919}920921function loadConfig() {922 try {923 const data = localStorage.getItem(`userConfig:${VERSION}`)924 return data ? JSON.parse(data) : null925 } catch {926 return null927 }928}929930// Migration from v1 to v2931function migrate() {932 try {933 const v1 = localStorage.getItem('userConfig:v1')934 if (v1) {935 const old = JSON.parse(v1)936 saveConfig({ theme: old.darkMode ? 'dark' : 'light', language: old.lang })937 localStorage.removeItem('userConfig:v1')938 }939 } catch {}940}941```942943**Store minimal fields from server responses:**944945```typescript946// User object has 20+ fields, only store what UI needs947function cachePrefs(user: FullUser) {948 try {949 localStorage.setItem('prefs:v1', JSON.stringify({950 theme: user.preferences.theme,951 notifications: user.preferences.notifications952 }))953 } catch {}954}955```956957**Always wrap in try-catch:** `getItem()` and `setItem()` throw in incognito/private browsing (Safari, Firefox), when quota exceeded, or when disabled.958959**Benefits:** Schema evolution via versioning, reduced storage size, prevents storing tokens/PII/internal flags.960<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:client-localstorage-schema:end -->961962<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:client-passive-event-listeners:start -->963## Use Passive Event Listeners for Scrolling Performance964965Add `{ 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.966967**Incorrect:**968969```typescript970useEffect(() => {971 const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)972 const handleWheel = (e: WheelEvent) => console.log(e.deltaY)973974 document.addEventListener('touchstart', handleTouch)975 document.addEventListener('wheel', handleWheel)976977 return () => {978 document.removeEventListener('touchstart', handleTouch)979 document.removeEventListener('wheel', handleWheel)980 }981}, [])982```983984**Correct:**985986```typescript987useEffect(() => {988 const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)989 const handleWheel = (e: WheelEvent) => console.log(e.deltaY)990991 document.addEventListener('touchstart', handleTouch, { passive: true })992 document.addEventListener('wheel', handleWheel, { passive: true })993994 return () => {995 document.removeEventListener('touchstart', handleTouch)996 document.removeEventListener('wheel', handleWheel)997 }998}, [])999```10001001**Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`.10021003**Don't use passive when:** implementing custom swipe gestures, custom zoom controls, or any listener that needs `preventDefault()`.1004<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:client-passive-event-listeners:end -->10051006<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:client-swr-dedup:start -->1007## Use SWR for Automatic Deduplication10081009SWR enables request deduplication, caching, and revalidation across component instances.10101011**Incorrect (no deduplication, each instance fetches):**10121013```tsx1014function UserList() {1015 const [users, setUsers] = useState([])1016 useEffect(() => {1017 fetch('/api/users')1018 .then(r => r.json())1019 .then(setUsers)1020 }, [])1021}1022```10231024**Correct (multiple instances share one request):**10251026```tsx1027import useSWR from 'swr'10281029function UserList() {1030 const { data: users } = useSWR('/api/users', fetcher)1031}1032```10331034**For immutable data:**10351036```tsx1037import { useImmutableSWR } from '@/lib/swr'10381039function StaticContent() {1040 const { data } = useImmutableSWR('/api/config', fetcher)1041}1042```10431044**For mutations:**10451046```tsx1047import { useSWRMutation } from 'swr/mutation'10481049function UpdateButton() {1050 const { trigger } = useSWRMutation('/api/user', updateUser)1051 return <button onClick={() => trigger()}>Update</button>1052}1053```10541055Reference: [https://swr.vercel.app](https://swr.vercel.app)1056<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:client-swr-dedup:end -->10571058<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:js-batch-dom-css:start -->1059## Avoid Layout Thrashing10601061Avoid 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.10621063**This is OK (browser batches style changes):**1064```typescript1065function updateElementStyles(element: HTMLElement) {1066 // Each line invalidates style, but browser batches the recalculation1067 element.style.width = '100px'1068 element.style.height = '200px'1069 element.style.backgroundColor = 'blue'1070 element.style.border = '1px solid black'1071}1072```10731074**Incorrect (interleaved reads and writes force reflows):**1075```typescript1076function layoutThrashing(element: HTMLElement) {1077 element.style.width = '100px'1078 const width = element.offsetWidth // Forces reflow1079 element.style.height = '200px'1080 const height = element.offsetHeight // Forces another reflow1081}1082```10831084**Correct (batch writes, then read once):**1085```typescript1086function updateElementStyles(element: HTMLElement) {1087 // Batch all writes together1088 element.style.width = '100px'1089 element.style.height = '200px'1090 element.style.backgroundColor = 'blue'1091 element.style.border = '1px solid black'10921093 // Read after all writes are done (single reflow)1094 const { width, height } = element.getBoundingClientRect()1095}1096```10971098**Correct (batch reads, then writes):**1099```typescript1100function avoidThrashing(element: HTMLElement) {1101 // Read phase - all layout queries first1102 const rect1 = element.getBoundingClientRect()1103 const offsetWidth = element.offsetWidth1104 const offsetHeight = element.offsetHeight11051106 // Write phase - all style changes after1107 element.style.width = '100px'1108 element.style.height = '200px'1109}1110```11111112**Better: use CSS classes**1113```css1114.highlighted-box {1115 width: 100px;1116 height: 200px;1117 background-color: blue;1118 border: 1px solid black;1119}1120```1121```typescript1122function updateElementStyles(element: HTMLElement) {1123 element.classList.add('highlighted-box')11241125 const { width, height } = element.getBoundingClientRect()1126}1127```11281129**React example:**1130```tsx1131// Incorrect: interleaving style changes with layout queries1132function Box({ isHighlighted }: { isHighlighted: boolean }) {1133 const ref = useRef<HTMLDivElement>(null)11341135 useEffect(() => {1136 if (ref.current && isHighlighted) {1137 ref.current.style.width = '100px'1138 const width = ref.current.offsetWidth // Forces layout1139 ref.current.style.height = '200px'1140 }1141 }, [isHighlighted])11421143 return <div ref={ref}>Content</div>1144}11451146// Correct: toggle class1147function Box({ isHighlighted }: { isHighlighted: boolean }) {1148 return (1149 <div className={isHighlighted ? 'highlighted-box' : ''}>1150 Content1151 </div>1152 )1153}1154```11551156Prefer 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.11571158See [this gist](https://gist.github.com/paulirish/5d52fb081b3570c81e3a) and [CSS Triggers](https://csstriggers.com/) for more information on layout-forcing operations.1159<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:js-batch-dom-css:end -->11601161<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:js-cache-function-results:start -->1162## Cache Repeated Function Calls11631164Use a module-level Map to cache function results when the same function is called repeatedly with the same inputs during render.11651166**Incorrect (redundant computation):**11671168```typescript1169function ProjectList({ projects }: { projects: Project[] }) {1170 return (1171 <div>1172 {projects.map(project => {1173 // slugify() called 100+ times for same project names1174 const slug = slugify(project.name)11751176 return <ProjectCard key={project.id} slug={slug} />1177 })}1178 </div>1179 )1180}1181```11821183**Correct (cached results):**11841185```typescript1186// Module-level cache1187const slugifyCache = new Map<string, string>()11881189function cachedSlugify(text: string): string {1190 if (slugifyCache.has(text)) {1191 return slugifyCache.get(text)!1192 }1193 const result = slugify(text)1194 slugifyCache.set(text, result)1195 return result1196}11971198function ProjectList({ projects }: { projects: Project[] }) {1199 return (1200 <div>1201 {projects.map(project => {1202 // Computed only once per unique project name1203 const slug = cachedSlugify(project.name)12041205 return <ProjectCard key={project.id} slug={slug} />1206 })}1207 </div>1208 )1209}1210```12111212**Simpler pattern for single-value functions:**12131214```typescript1215let isLoggedInCache: boolean | null = null12161217function isLoggedIn(): boolean {1218 if (isLoggedInCache !== null) {1219 return isLoggedInCache1220 }12211222 isLoggedInCache = document.cookie.includes('auth=')1223 return isLoggedInCache1224}12251226// Clear cache when auth changes1227function onAuthChange() {1228 isLoggedInCache = null1229}1230```12311232Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.12331234Reference: [How we made the Vercel Dashboard twice as fast](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast)1235<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:js-cache-function-results:end -->12361237<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:js-cache-property-access:start -->1238## Cache Property Access in Loops12391240Cache object property lookups in hot paths.12411242**Incorrect (3 lookups × N iterations):**12431244```typescript1245for (let i = 0; i < arr.length; i++) {1246 process(obj.config.settings.value)1247}1248```12491250**Correct (1 lookup total):**12511252```typescript1253const value = obj.config.settings.value1254const len = arr.length1255for (let i = 0; i < len; i++) {1256 process(value)1257}1258```1259<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:js-cache-property-access:end -->12601261<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:js-cache-storage:start -->1262## Cache Storage API Calls12631264`localStorage`, `sessionStorage`, and `document.cookie` are synchronous and expensive. Cache reads in memory.12651266**Incorrect (reads storage on every call):**12671268```typescript1269function getTheme() {1270 return localStorage.getItem('theme') ?? 'light'1271}1272// Called 10 times = 10 storage reads1273```12741275**Correct (Map cache):**12761277```typescript1278const storageCache = new Map<string, string | null>()12791280function getLocalStorage(key: string) {1281 if (!storageCache.has(key)) {1282 storageCache.set(key, localStorage.getItem(key))1283 }1284 return storageCache.get(key)1285}12861287function setLocalStorage(key: string, value: string) {1288 localStorage.setItem(key, value)1289 storageCache.set(key, value) // keep cache in sync1290}1291```12921293Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.12941295**Cookie caching:**12961297```typescript1298let cookieCache: Record<string, string> | null = null12991300function getCookie(name: string) {1301 if (!cookieCache) {1302 cookieCache = Object.fromEntries(1303 document.cookie.split('; ').map(c => c.split('='))1304 )1305 }1306 return cookieCache[name]1307}1308```13091310**Important (invalidate on external changes):**13111312If storage can change externally (another tab, server-set cookies), invalidate cache:13131314```typescript1315window.addEventListener('storage', (e) => {1316 if (e.key) storageCache.delete(e.key)1317})13181319document.addEventListener('visibilitychange', () => {1320 if (document.visibilityState === 'visible') {1321 storageCache.clear()1322 }1323})1324```1325<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:js-cache-storage:end -->13261327<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:js-combine-iterations:start -->1328## Combine Multiple Array Iterations13291330Multiple `.filter()` or `.map()` calls iterate the array multiple times. Combine into one loop.13311332**Incorrect (3 iterations):**13331334```typescript1335const admins = users.filter(u => u.isAdmin)1336const testers = users.filter(u => u.isTester)1337const inactive = users.filter(u => !u.isActive)1338```13391340**Correct (1 iteration):**13411342```typescript1343const admins: User[] = []1344const testers: User[] = []1345const inactive: User[] = []13461347for (const user of users) {1348 if (user.isAdmin) admins.push(user)1349 if (user.isTester) testers.push(user)1350 if (!user.isActive) inactive.push(user)1351}1352```1353<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:js-combine-iterations:end -->13541355<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:js-early-exit:start -->1356## Early Return from Functions13571358Return early when result is determined to skip unnecessary processing.13591360**Incorrect (processes all items even after finding answer):**13611362```typescript1363function validateUsers(users: User[]) {1364 let hasError = false1365 let errorMessage = ''13661367 for (const user of users) {1368 if (!user.email) {1369 hasError = true1370 errorMessage = 'Email required'1371 }1372 if (!user.name) {1373 hasError = true1374 errorMessage = 'Name required'1375 }1376 // Continues checking all users even after error found1377 }13781379 return hasError ? { valid: false, error: errorMessage } : { valid: true }1380}1381```13821383**Correct (returns immediately on first error):**13841385```typescript1386function validateUsers(users: User[]) {1387 for (const user of users) {1388 if (!user.email) {1389 return { valid: false, error: 'Email required' }1390 }1391 if (!user.name) {1392 return { valid: false, error: 'Name required' }1393 }1394 }13951396 return { valid: true }1397}1398```1399<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:js-early-exit:end -->14001401<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:js-flatmap-filter:start -->1402## Use flatMap to Map and Filter in One Pass14031404**Impact: LOW-MEDIUM (eliminates intermediate array)**14051406Chaining `.map().filter(Boolean)` creates an intermediate array and iterates twice. Use `.flatMap()` to transform and filter in a single pass.14071408**Incorrect (2 iterations, intermediate array):**14091410```typescript1411const userNames = users1412 .map(user => user.isActive ? user.name : null)1413 .filter(Boolean)1414```14151416**Correct (1 iteration, no intermediate array):**14171418```typescript1419const userNames = users.flatMap(user =>1420 user.isActive ? [user.name] : []1421)1422```14231424**More examples:**14251426```typescript1427// Extract valid emails from responses1428// Before1429const emails = responses1430 .map(r => r.success ? r.data.email : null)1431 .filter(Boolean)14321433// After1434const emails = responses.flatMap(r =>1435 r.success ? [r.data.email] : []1436)14371438// Parse and filter valid numbers1439// Before1440const numbers = strings1441 .map(s => parseInt(s, 10))1442 .filter(n => !isNaN(n))14431444// After1445const numbers = strings.flatMap(s => {1446 const n = parseInt(s, 10)1447 return isNaN(n) ? [] : [n]1448})1449```14501451**When to use:**1452- Transforming items while filtering some out1453- Conditional mapping where some inputs produce no output1454- Parsing/validating where invalid inputs should be skipped1455<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:js-flatmap-filter:end -->14561457<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:js-hoist-regexp:start -->1458## Hoist RegExp Creation14591460Don't create RegExp inside render. Hoist to module scope or memoize with `useMemo()`.14611462**Incorrect (new RegExp every render):**14631464```tsx1465function Highlighter({ text, query }: Props) {1466 const regex = new RegExp(`(${query})`, 'gi')1467 const parts = text.split(regex)1468 return <>{parts.map((part, i) => ...)}</>1469}1470```14711472**Correct (memoize or hoist):**14731474```tsx1475const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/14761477function Highlighter({ text, query }: Props) {1478 const regex = useMemo(1479 () => new RegExp(`(${escapeRegex(query)})`, 'gi'),1480 [query]1481 )1482 const parts = text.split(regex)1483 return <>{parts.map((part, i) => ...)}</>1484}1485```14861487**Warning (global regex has mutable state):**14881489Global regex (`/g`) has mutable `lastIndex` state:14901491```typescript1492const regex = /foo/g1493regex.test('foo') // true, lastIndex = 31494regex.test('foo') // false, lastIndex = 01495```1496<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:js-hoist-regexp:end -->14971498<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:js-index-maps:start -->1499## Build Index Maps for Repeated Lookups15001501Multiple `.find()` calls by the same key should use a Map.15021503**Incorrect (O(n) per lookup):**15041505```typescript1506function processOrders(orders: Order[], users: User[]) {1507 return orders.map(order => ({1508 ...order,1509 user: users.find(u => u.id === order.userId)1510 }))1511}1512```15131514**Correct (O(1) per lookup):**15151516```typescript1517function processOrders(orders: Order[], users: User[]) {1518 const userById = new Map(users.map(u => [u.id, u]))15191520 return orders.map(order => ({1521 ...order,1522 user: userById.get(order.userId)1523 }))1524}1525```15261527Build map once (O(n)), then all lookups are O(1).1528For 1000 orders × 1000 users: 1M ops → 2K ops.1529<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:js-index-maps:end -->15301531<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:js-length-check-first:start -->1532## Early Length Check for Array Comparisons15331534When comparing arrays with expensive operations (sorting, deep equality, serialization), check lengths first. If lengths differ, the arrays cannot be equal.15351536In real-world applications, this optimization is especially valuable when the comparison runs in hot paths (event handlers, render loops).15371538**Incorrect (always runs expensive comparison):**15391540```typescript1541function hasChanges(current: string[], original: string[]) {1542 // Always sorts and joins, even when lengths differ1543 return current.sort().join() !== original.sort().join()1544}1545```15461547Two 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.15481549**Correct (O(1) length check first):**15501551```typescript1552function hasChanges(current: string[], original: string[]) {1553 // Early return if lengths differ1554 if (current.length !== original.length) {1555 return true1556 }1557 // Only sort when lengths match1558 const currentSorted = current.toSorted()1559 const originalSorted = original.toSorted()1560 for (let i = 0; i < currentSorted.length; i++) {1561 if (currentSorted[i] !== originalSorted[i]) {1562 return true1563 }1564 }1565 return false1566}1567```15681569This new approach is more efficient because:1570- It avoids the overhead of sorting and joining the arrays when lengths differ1571- It avoids consuming memory for the joined strings (especially important for large arrays)1572- It avoids mutating the original arrays1573- It returns early when a difference is found1574<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:js-length-check-first:end -->15751576<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:js-min-max-loop:start -->1577## Use Loop for Min/Max Instead of Sort15781579Finding the smallest or largest element only requires a single pass through the array. Sorting is wasteful and slower.15801581**Incorrect (O(n log n) - sort to find latest):**15821583```typescript1584interface Project {1585 id: string1586 name: string1587 updatedAt: number1588}15891590function getLatestProject(projects: Project[]) {1591 const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt)1592 return sorted[0]1593}1594```15951596Sorts the entire array just to find the maximum value.15971598**Incorrect (O(n log n) - sort for oldest and newest):**15991600```typescript1601function getOldestAndNewest(projects: Project[]) {1602 const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt)1603 return { oldest: sorted[0], newest: sorted[sorted.length - 1] }1604}1605```16061607Still sorts unnecessarily when only min/max are needed.16081609**Correct (O(n) - single loop):**16101611```typescript1612function getLatestProject(projects: Project[]) {1613 if (projects.length === 0) return null16141615 let latest = projects[0]16161617 for (let i = 1; i < projects.length; i++) {1618 if (projects[i].updatedAt > latest.updatedAt) {1619 latest = projects[i]1620 }1621 }16221623 return latest1624}16251626function getOldestAndNewest(projects: Project[]) {1627 if (projects.length === 0) return { oldest: null, newest: null }16281629 let oldest = projects[0]1630 let newest = projects[0]16311632 for (let i = 1; i < projects.length; i++) {1633 if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i]1634 if (projects[i].updatedAt > newest.updatedAt) newest = projects[i]1635 }16361637 return { oldest, newest }1638}1639```16401641Single pass through the array, no copying, no sorting.16421643**Alternative (Math.min/Math.max for small arrays):**16441645```typescript1646const numbers = [5, 2, 8, 1, 9]1647const min = Math.min(...numbers)1648const max = Math.max(...numbers)1649```16501651This 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.1652<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:js-min-max-loop:end -->16531654<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:js-request-idle-callback:start -->1655## Defer Non-Critical Work with requestIdleCallback16561657**Impact: MEDIUM (keeps UI responsive during background tasks)**16581659Use `requestIdleCallback()` to schedule non-critical work during browser idle periods. This keeps the main thread free for user interactions and animations, reducing jank and improving perceived performance.16601661**Incorrect (blocks main thread during user interaction):**16621663```typescript1664function handleSearch(query: string) {1665 const results = searchItems(query)1666 setResults(results)16671668 // These block the main thread immediately1669 analytics.track('search', { query })1670 saveToRecentSearches(query)1671 prefetchTopResults(results.slice(0, 3))1672}1673```16741675**Correct (defers non-critical work to idle time):**16761677```typescript1678function handleSearch(query: string) {1679 const results = searchItems(query)1680 setResults(results)16811682 // Defer non-critical work to idle periods1683 requestIdleCallback(() => {1684 analytics.track('search', { query })1685 })16861687 requestIdleCallback(() => {1688 saveToRecentSearches(query)1689 })16901691 requestIdleCallback(() => {1692 prefetchTopResults(results.slice(0, 3))1693 })1694}1695```16961697**With timeout for required work:**16981699```typescript1700// Ensure analytics fires within 2 seconds even if browser stays busy1701requestIdleCallback(1702 () => analytics.track('page_view', { path: location.pathname }),1703 { timeout: 2000 }1704)1705```17061707**Chunking large tasks:**17081709```typescript1710function processLargeDataset(items: Item[]) {1711 let index = 017121713 function processChunk(deadline: IdleDeadline) {1714 // Process items while we have idle time (aim for <50ms chunks)1715 while (index < items.length && deadline.timeRemaining() > 0) {1716 processItem(items[index])1717 index++1718 }17191720 // Schedule next chunk if more items remain1721 if (index < items.length) {1722 requestIdleCallback(processChunk)1723 }1724 }17251726 requestIdleCallback(processChunk)1727}1728```17291730**With fallback for unsupported browsers:**17311732```typescript1733const scheduleIdleWork = window.requestIdleCallback ?? ((cb: () => void) => setTimeout(cb, 1))17341735scheduleIdleWork(() => {1736 // Non-critical work1737})1738```17391740**When to use:**17411742- Analytics and telemetry1743- Saving state to localStorage/IndexedDB1744- Prefetching resources for likely next actions1745- Processing non-urgent data transformations1746- Lazy initialization of non-critical features17471748**When NOT to use:**17491750- User-initiated actions that need immediate feedback1751- Rendering updates the user is waiting for1752- Time-sensitive operations1753<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:js-request-idle-callback:end -->17541755<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:js-set-map-lookups:start -->1756## Use Set/Map for O(1) Lookups17571758Convert arrays to Set/Map for repeated membership checks.17591760**Incorrect (O(n) per check):**17611762```typescript1763const allowedIds = ['a', 'b', 'c', ...]1764items.filter(item => allowedIds.includes(item.id))1765```17661767**Correct (O(1) per check):**17681769```typescript1770const allowedIds = new Set(['a', 'b', 'c', ...])1771items.filter(item => allowedIds.has(item.id))1772```1773<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:js-set-map-lookups:end -->17741775<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:js-tosorted-immutable:start -->1776## Use toSorted() Instead of sort() for Immutability17771778`.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.17791780**Incorrect (mutates original array):**17811782```typescript1783function UserList({ users }: { users: User[] }) {1784 // Mutates the users prop array!1785 const sorted = useMemo(1786 () => users.sort((a, b) => a.name.localeCompare(b.name)),1787 [users]1788 )1789 return <div>{sorted.map(renderUser)}</div>1790}1791```17921793**Correct (creates new array):**17941795```typescript1796function UserList({ users }: { users: User[] }) {1797 // Creates new sorted array, original unchanged1798 const sorted = useMemo(1799 () => users.toSorted((a, b) => a.name.localeCompare(b.name)),1800 [users]1801 )1802 return <div>{sorted.map(renderUser)}</div>1803}1804```18051806**Why this matters in React:**180718081. Props/state mutations break React's immutability model - React expects props and state to be treated as read-only18092. Causes stale closure bugs - Mutating arrays inside closures (callbacks, effects) can lead to unexpected behavior18101811**Browser support (fallback for older browsers):**18121813`.toSorted()` is available in all modern browsers (Chrome 110+, Safari 16+, Firefox 115+, Node.js 20+). For older environments, use spread operator:18141815```typescript1816// Fallback for older browsers1817const sorted = [...items].sort((a, b) => a.value - b.value)1818```18191820**Other immutable array methods:**18211822- `.toSorted()` - immutable sort1823- `.toReversed()` - immutable reverse1824- `.toSpliced()` - immutable splice1825- `.with()` - immutable element replacement1826<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:js-tosorted-immutable:end -->18271828<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rendering-activity:start -->1829## Use Activity Component for Show/Hide18301831Use React's `<Activity>` to preserve state/DOM for expensive components that frequently toggle visibility.18321833**Usage:**18341835```tsx1836import { Activity } from 'react'18371838function Dropdown({ isOpen }: Props) {1839 return (1840 <Activity mode={isOpen ? 'visible' : 'hidden'}>1841 <ExpensiveMenu />1842 </Activity>1843 )1844}1845```18461847Avoids expensive re-renders and state loss.1848<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rendering-activity:end -->18491850<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rendering-animate-svg-wrapper:start -->1851## Animate SVG Wrapper Instead of SVG Element18521853Many browsers don't have hardware acceleration for CSS3 animations on SVG elements. Wrap SVG in a `<div>` and animate the wrapper instead.18541855**Incorrect (animating SVG directly - no hardware acceleration):**18561857```tsx1858function LoadingSpinner() {1859 return (1860 <svg1861 className="animate-spin"1862 width="24"1863 height="24"1864 viewBox="0 0 24 24"1865 >1866 <circle cx="12" cy="12" r="10" stroke="currentColor" />1867 </svg>1868 )1869}1870```18711872**Correct (animating wrapper div - hardware accelerated):**18731874```tsx1875function LoadingSpinner() {1876 return (1877 <div className="animate-spin">1878 <svg1879 width="24"1880 height="24"1881 viewBox="0 0 24 24"1882 >1883 <circle cx="12" cy="12" r="10" stroke="currentColor" />1884 </svg>1885 </div>1886 )1887}1888```18891890This applies to all CSS transforms and transitions (`transform`, `opacity`, `translate`, `scale`, `rotate`). The wrapper div allows browsers to use GPU acceleration for smoother animations.1891<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rendering-animate-svg-wrapper:end -->18921893<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rendering-conditional-render:start -->1894## Use Explicit Conditional Rendering18951896Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render.18971898**Incorrect (renders "0" when count is 0):**18991900```tsx1901function Badge({ count }: { count: number }) {1902 return (1903 <div>1904 {count && <span className="badge">{count}</span>}1905 </div>1906 )1907}19081909// When count = 0, renders: <div>0</div>1910// When count = 5, renders: <div><span class="badge">5</span></div>1911```19121913**Correct (renders nothing when count is 0):**19141915```tsx1916function Badge({ count }: { count: number }) {1917 return (1918 <div>1919 {count > 0 ? <span className="badge">{count}</span> : null}1920 </div>1921 )1922}19231924// When count = 0, renders: <div></div>1925// When count = 5, renders: <div><span class="badge">5</span></div>1926```1927<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rendering-conditional-render:end -->19281929<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rendering-content-visibility:start -->1930## CSS content-visibility for Long Lists19311932Apply `content-visibility: auto` to defer off-screen rendering.19331934**CSS:**19351936```css1937.message-item {1938 content-visibility: auto;1939 contain-intrinsic-size: 0 80px;1940}1941```19421943**Example:**19441945```tsx1946function MessageList({ messages }: { messages: Message[] }) {1947 return (1948 <div className="overflow-y-auto h-screen">1949 {messages.map(msg => (1950 <div key={msg.id} className="message-item">1951 <Avatar user={msg.author} />1952 <div>{msg.content}</div>1953 </div>1954 ))}1955 </div>1956 )1957}1958```19591960For 1000 messages, browser skips layout/paint for ~990 off-screen items (10× faster initial render).1961<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rendering-content-visibility:end -->19621963<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rendering-hoist-jsx:start -->1964## Hoist Static JSX Elements19651966Extract static JSX outside components to avoid re-creation.19671968**Incorrect (recreates element every render):**19691970```tsx1971function LoadingSkeleton() {1972 return <div className="animate-pulse h-20 bg-gray-200" />1973}19741975function Container() {1976 return (1977 <div>1978 {loading && <LoadingSkeleton />}1979 </div>1980 )1981}1982```19831984**Correct (reuses same element):**19851986```tsx1987const loadingSkeleton = (1988 <div className="animate-pulse h-20 bg-gray-200" />1989)19901991function Container() {1992 return (1993 <div>1994 {loading && loadingSkeleton}1995 </div>1996 )1997}1998```19992000This is especially helpful for large and static SVG nodes, which can be expensive to recreate on every render.20012002**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.2003<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rendering-hoist-jsx:end -->20042005<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rendering-hydration-no-flicker:start -->2006## Prevent Hydration Mismatch Without Flickering20072008When 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.20092010**Incorrect (breaks SSR):**20112012```tsx2013function ThemeWrapper({ children }: { children: ReactNode }) {2014 // localStorage is not available on server - throws error2015 const theme = localStorage.getItem('theme') || 'light'20162017 return (2018 <div className={theme}>2019 {children}2020 </div>2021 )2022}2023```20242025Server-side rendering will fail because `localStorage` is undefined.20262027**Incorrect (visual flickering):**20282029```tsx2030function ThemeWrapper({ children }: { children: ReactNode }) {2031 const [theme, setTheme] = useState('light')20322033 useEffect(() => {2034 // Runs after hydration - causes visible flash2035 const stored = localStorage.getItem('theme')2036 if (stored) {2037 setTheme(stored)2038 }2039 }, [])20402041 return (2042 <div className={theme}>2043 {children}2044 </div>2045 )2046}2047```20482049Component first renders with default value (`light`), then updates after hydration, causing a visible flash of incorrect content.20502051**Correct (no flicker, no hydration mismatch):**20522053```tsx2054function ThemeWrapper({ children }: { children: ReactNode }) {2055 return (2056 <>2057 <div id="theme-wrapper">2058 {children}2059 </div>2060 <script2061 dangerouslySetInnerHTML={{2062 __html: `2063 (function() {2064 try {2065 var theme = localStorage.getItem('theme') || 'light';2066 var el = document.getElementById('theme-wrapper');2067 if (el) el.className = theme;2068 } catch (e) {}2069 })();2070 `,2071 }}2072 />2073 </>2074 )2075}2076```20772078The inline script executes synchronously before showing the element, ensuring the DOM already has the correct value. No flickering, no hydration mismatch.20792080This pattern is especially useful for theme toggles, user preferences, authentication states, and any client-only data that should render immediately without flashing default values.2081<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rendering-hydration-no-flicker:end -->20822083<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rendering-hydration-suppress-warning:start -->2084## Suppress Expected Hydration Mismatches20852086In 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.20872088**Incorrect (known mismatch warnings):**20892090```tsx2091function Timestamp() {2092 return <span>{new Date().toLocaleString()}</span>2093}2094```20952096**Correct (suppress expected mismatch only):**20972098```tsx2099function Timestamp() {2100 return (2101 <span suppressHydrationWarning>2102 {new Date().toLocaleString()}2103 </span>2104 )2105}2106```2107<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rendering-hydration-suppress-warning:end -->21082109<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rendering-resource-hints:start -->2110## Use React DOM Resource Hints21112112**Impact: HIGH (reduces load time for critical resources)**21132114React DOM provides APIs to hint the browser about resources it will need. These are especially useful in server components to start loading resources before the client even receives the HTML.21152116- **`prefetchDNS(href)`**: Resolve DNS for a domain you expect to connect to2117- **`preconnect(href)`**: Establish connection (DNS + TCP + TLS) to a server2118- **`preload(href, options)`**: Fetch a resource (stylesheet, font, script, image) you'll use soon2119- **`preloadModule(href)`**: Fetch an ES module you'll use soon2120- **`preinit(href, options)`**: Fetch and evaluate a stylesheet or script2121- **`preinitModule(href)`**: Fetch and evaluate an ES module21222123**Example (preconnect to third-party APIs):**21242125```tsx2126import { preconnect, prefetchDNS } from 'react-dom'21272128export default function App() {2129 prefetchDNS('https://analytics.example.com')2130 preconnect('https://api.example.com')21312132 return <main>{/* content */}</main>2133}2134```21352136**Example (preload critical fonts and styles):**21372138```tsx2139import { preload, preinit } from 'react-dom'21402141export default function RootLayout({ children }) {2142 // Preload font file2143 preload('/fonts/inter.woff2', { as: 'font', type: 'font/woff2', crossOrigin: 'anonymous' })21442145 // Fetch and apply critical stylesheet immediately2146 preinit('/styles/critical.css', { as: 'style' })21472148 return (2149 <html>2150 <body>{children}</body>2151 </html>2152 )2153}2154```21552156**Example (preload modules for code-split routes):**21572158```tsx2159import { preloadModule, preinitModule } from 'react-dom'21602161function Navigation() {2162 const preloadDashboard = () => {2163 preloadModule('/dashboard.js', { as: 'script' })2164 }21652166 return (2167 <nav>2168 <a href="/dashboard" onMouseEnter={preloadDashboard}>2169 Dashboard2170 </a>2171 </nav>2172 )2173}2174```21752176**When to use each:**21772178| API | Use case |2179|-----|----------|2180| `prefetchDNS` | Third-party domains you'll connect to later |2181| `preconnect` | APIs or CDNs you'll fetch from immediately |2182| `preload` | Critical resources needed for current page |2183| `preloadModule` | JS modules for likely next navigation |2184| `preinit` | Stylesheets/scripts that must execute early |2185| `preinitModule` | ES modules that must execute early |21862187Reference: [React DOM Resource Preloading APIs](https://react.dev/reference/react-dom#resource-preloading-apis)2188<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rendering-resource-hints:end -->21892190<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rendering-script-defer-async:start -->2191## Use defer or async on Script Tags21922193**Impact: HIGH (eliminates render-blocking)**21942195Script tags without `defer` or `async` block HTML parsing while the script downloads and executes. This delays First Contentful Paint and Time to Interactive.21962197- **`defer`**: Downloads in parallel, executes after HTML parsing completes, maintains execution order2198- **`async`**: Downloads in parallel, executes immediately when ready, no guaranteed order21992200Use `defer` for scripts that depend on DOM or other scripts. Use `async` for independent scripts like analytics.22012202**Incorrect (blocks rendering):**22032204```tsx2205export default function Document() {2206 return (2207 <html>2208 <head>2209 <script src="https://example.com/analytics.js" />2210 <script src="/scripts/utils.js" />2211 </head>2212 <body>{/* content */}</body>2213 </html>2214 )2215}2216```22172218**Correct (non-blocking):**22192220```tsx2221export default function Document() {2222 return (2223 <html>2224 <head>2225 {/* Independent script - use async */}2226 <script src="https://example.com/analytics.js" async />2227 {/* DOM-dependent script - use defer */}2228 <script src="/scripts/utils.js" defer />2229 </head>2230 <body>{/* content */}</body>2231 </html>2232 )2233}2234```22352236**Note:** In Next.js, prefer the `next/script` component with `strategy` prop instead of raw script tags:22372238```tsx2239import Script from 'next/script'22402241export default function Page() {2242 return (2243 <>2244 <Script src="https://example.com/analytics.js" strategy="afterInteractive" />2245 <Script src="/scripts/utils.js" strategy="beforeInteractive" />2246 </>2247 )2248}2249```22502251Reference: [MDN - Script element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#defer)2252<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rendering-script-defer-async:end -->22532254<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rendering-svg-precision:start -->2255## Optimize SVG Precision22562257Reduce SVG coordinate precision to decrease file size. The optimal precision depends on the viewBox size, but in general reducing precision should be considered.22582259**Incorrect (excessive precision):**22602261```svg2262<path d="M 10.293847 20.847362 L 30.938472 40.192837" />2263```22642265**Correct (1 decimal place):**22662267```svg2268<path d="M 10.3 20.8 L 30.9 40.2" />2269```22702271**Automate with SVGO:**22722273```bash2274npx svgo --precision=1 --multipass icon.svg2275```2276<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rendering-svg-precision:end -->22772278<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rendering-usetransition-loading:start -->2279## Use useTransition Over Manual Loading States22802281Use `useTransition` instead of manual `useState` for loading states. This provides built-in `isPending` state and automatically manages transitions.22822283**Incorrect (manual loading state):**22842285```tsx2286function SearchResults() {2287 const [query, setQuery] = useState('')2288 const [results, setResults] = useState([])2289 const [isLoading, setIsLoading] = useState(false)22902291 const handleSearch = async (value: string) => {2292 setIsLoading(true)2293 setQuery(value)2294 const data = await fetchResults(value)2295 setResults(data)2296 setIsLoading(false)2297 }22982299 return (2300 <>2301 <input onChange={(e) => handleSearch(e.target.value)} />2302 {isLoading && <Spinner />}2303 <ResultsList results={results} />2304 </>2305 )2306}2307```23082309**Correct (useTransition with built-in pending state):**23102311```tsx2312import { useTransition, useState } from 'react'23132314function SearchResults() {2315 const [query, setQuery] = useState('')2316 const [results, setResults] = useState([])2317 const [isPending, startTransition] = useTransition()23182319 const handleSearch = (value: string) => {2320 setQuery(value) // Update input immediately23212322 startTransition(async () => {2323 // Fetch and update results2324 const data = await fetchResults(value)2325 setResults(data)2326 })2327 }23282329 return (2330 <>2331 <input onChange={(e) => handleSearch(e.target.value)} />2332 {isPending && <Spinner />}2333 <ResultsList results={results} />2334 </>2335 )2336}2337```23382339**Benefits:**23402341- **Automatic pending state**: No need to manually manage `setIsLoading(true/false)`2342- **Error resilience**: Pending state correctly resets even if the transition throws2343- **Better responsiveness**: Keeps the UI responsive during updates2344- **Interrupt handling**: New transitions automatically cancel pending ones23452346Reference: [useTransition](https://react.dev/reference/react/useTransition)2347<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rendering-usetransition-loading:end -->23482349<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rerender-defer-reads:start -->2350## Defer State Reads to Usage Point23512352Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks.23532354**Incorrect (subscribes to all searchParams changes):**23552356```tsx2357function ShareButton({ chatId }: { chatId: string }) {2358 const searchParams = useSearchParams()23592360 const handleShare = () => {2361 const ref = searchParams.get('ref')2362 shareChat(chatId, { ref })2363 }23642365 return <button onClick={handleShare}>Share</button>2366}2367```23682369**Correct (reads on demand, no subscription):**23702371```tsx2372function ShareButton({ chatId }: { chatId: string }) {2373 const handleShare = () => {2374 const params = new URLSearchParams(window.location.search)2375 const ref = params.get('ref')2376 shareChat(chatId, { ref })2377 }23782379 return <button onClick={handleShare}>Share</button>2380}2381```2382<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rerender-defer-reads:end -->23832384<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rerender-dependencies:start -->2385## Narrow Effect Dependencies23862387Specify primitive dependencies instead of objects to minimize effect re-runs.23882389**Incorrect (re-runs on any user field change):**23902391```tsx2392useEffect(() => {2393 console.log(user.id)2394}, [user])2395```23962397**Correct (re-runs only when id changes):**23982399```tsx2400useEffect(() => {2401 console.log(user.id)2402}, [user.id])2403```24042405**For derived state, compute outside effect:**24062407```tsx2408// Incorrect: runs on width=767, 766, 765...2409useEffect(() => {2410 if (width < 768) {2411 enableMobileMode()2412 }2413}, [width])24142415// Correct: runs only on boolean transition2416const isMobile = width < 7682417useEffect(() => {2418 if (isMobile) {2419 enableMobileMode()2420 }2421}, [isMobile])2422```2423<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rerender-dependencies:end -->24242425<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rerender-derived-state-no-effect:start -->2426## Calculate Derived State During Rendering24272428If 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.24292430**Incorrect (redundant state and effect):**24312432```tsx2433function Form() {2434 const [firstName, setFirstName] = useState('First')2435 const [lastName, setLastName] = useState('Last')2436 const [fullName, setFullName] = useState('')24372438 useEffect(() => {2439 setFullName(firstName + ' ' + lastName)2440 }, [firstName, lastName])24412442 return <p>{fullName}</p>2443}2444```24452446**Correct (derive during render):**24472448```tsx2449function Form() {2450 const [firstName, setFirstName] = useState('First')2451 const [lastName, setLastName] = useState('Last')2452 const fullName = firstName + ' ' + lastName24532454 return <p>{fullName}</p>2455}2456```24572458References: [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect)2459<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rerender-derived-state-no-effect:end -->24602461<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rerender-derived-state:start -->2462## Subscribe to Derived State24632464Subscribe to derived boolean state instead of continuous values to reduce re-render frequency.24652466**Incorrect (re-renders on every pixel change):**24672468```tsx2469function Sidebar() {2470 const width = useWindowWidth() // updates continuously2471 const isMobile = width < 7682472 return <nav className={isMobile ? 'mobile' : 'desktop'} />2473}2474```24752476**Correct (re-renders only when boolean changes):**24772478```tsx2479function Sidebar() {2480 const isMobile = useMediaQuery('(max-width: 767px)')2481 return <nav className={isMobile ? 'mobile' : 'desktop'} />2482}2483```2484<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rerender-derived-state:end -->24852486<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rerender-functional-setstate:start -->2487## Use Functional setState Updates24882489When 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.24902491**Incorrect (requires state as dependency):**24922493```tsx2494function TodoList() {2495 const [items, setItems] = useState(initialItems)24962497 // Callback must depend on items, recreated on every items change2498 const addItems = useCallback((newItems: Item[]) => {2499 setItems([...items, ...newItems])2500 }, [items]) // ❌ items dependency causes recreations25012502 // Risk of stale closure if dependency is forgotten2503 const removeItem = useCallback((id: string) => {2504 setItems(items.filter(item => item.id !== id))2505 }, []) // ❌ Missing items dependency - will use stale items!25062507 return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />2508}2509```25102511The 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.25122513**Correct (stable callbacks, no stale closures):**25142515```tsx2516function TodoList() {2517 const [items, setItems] = useState(initialItems)25182519 // Stable callback, never recreated2520 const addItems = useCallback((newItems: Item[]) => {2521 setItems(curr => [...curr, ...newItems])2522 }, []) // ✅ No dependencies needed25232524 // Always uses latest state, no stale closure risk2525 const removeItem = useCallback((id: string) => {2526 setItems(curr => curr.filter(item => item.id !== id))2527 }, []) // ✅ Safe and stable25282529 return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />2530}2531```25322533**Benefits:**253425351. **Stable callback references** - Callbacks don't need to be recreated when state changes25362. **No stale closures** - Always operates on the latest state value25373. **Fewer dependencies** - Simplifies dependency arrays and reduces memory leaks25384. **Prevents bugs** - Eliminates the most common source of React closure bugs25392540**When to use functional updates:**25412542- Any setState that depends on the current state value2543- Inside useCallback/useMemo when state is needed2544- Event handlers that reference state2545- Async operations that update state25462547**When direct updates are fine:**25482549- Setting state to a static value: `setCount(0)`2550- Setting state from props/arguments only: `setName(newName)`2551- State doesn't depend on previous value25522553**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.2554<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rerender-functional-setstate:end -->25552556<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rerender-lazy-state-init:start -->2557## Use Lazy State Initialization25582559Pass 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.25602561**Incorrect (runs on every render):**25622563```tsx2564function FilteredList({ items }: { items: Item[] }) {2565 // buildSearchIndex() runs on EVERY render, even after initialization2566 const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))2567 const [query, setQuery] = useState('')25682569 // When query changes, buildSearchIndex runs again unnecessarily2570 return <SearchResults index={searchIndex} query={query} />2571}25722573function UserProfile() {2574 // JSON.parse runs on every render2575 const [settings, setSettings] = useState(2576 JSON.parse(localStorage.getItem('settings') || '{}')2577 )25782579 return <SettingsForm settings={settings} onChange={setSettings} />2580}2581```25822583**Correct (runs only once):**25842585```tsx2586function FilteredList({ items }: { items: Item[] }) {2587 // buildSearchIndex() runs ONLY on initial render2588 const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))2589 const [query, setQuery] = useState('')25902591 return <SearchResults index={searchIndex} query={query} />2592}25932594function UserProfile() {2595 // JSON.parse runs only on initial render2596 const [settings, setSettings] = useState(() => {2597 const stored = localStorage.getItem('settings')2598 return stored ? JSON.parse(stored) : {}2599 })26002601 return <SettingsForm settings={settings} onChange={setSettings} />2602}2603```26042605Use lazy initialization when computing initial values from localStorage/sessionStorage, building data structures (indexes, maps), reading from the DOM, or performing heavy transformations.26062607For simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary.2608<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rerender-lazy-state-init:end -->26092610<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rerender-memo-with-default-value:start -->2611## Extract Default Non-primitive Parameter Value from Memoized Component to Constant26122613When 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()`.26142615To address this issue, extract the default value into a constant.26162617**Incorrect (`onClick` has different values on every rerender):**26182619```tsx2620const UserAvatar = memo(function UserAvatar({ onClick = () => {} }: { onClick?: () => void }) {2621 // ...2622})26232624// Used without optional onClick2625<UserAvatar />2626```26272628**Correct (stable default value):**26292630```tsx2631const NOOP = () => {};26322633const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) {2634 // ...2635})26362637// Used without optional onClick2638<UserAvatar />2639```2640<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rerender-memo-with-default-value:end -->26412642<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rerender-memo:start -->2643## Extract to Memoized Components26442645Extract expensive work into memoized components to enable early returns before computation.26462647**Incorrect (computes avatar even when loading):**26482649```tsx2650function Profile({ user, loading }: Props) {2651 const avatar = useMemo(() => {2652 const id = computeAvatarId(user)2653 return <Avatar id={id} />2654 }, [user])26552656 if (loading) return <Skeleton />2657 return <div>{avatar}</div>2658}2659```26602661**Correct (skips computation when loading):**26622663```tsx2664const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {2665 const id = useMemo(() => computeAvatarId(user), [user])2666 return <Avatar id={id} />2667})26682669function Profile({ user, loading }: Props) {2670 if (loading) return <Skeleton />2671 return (2672 <div>2673 <UserAvatar user={user} />2674 </div>2675 )2676}2677```26782679**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.2680<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rerender-memo:end -->26812682<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rerender-move-effect-to-event:start -->2683## Put Interaction Logic in Event Handlers26842685If 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.26862687**Incorrect (event modeled as state + effect):**26882689```tsx2690function Form() {2691 const [submitted, setSubmitted] = useState(false)2692 const theme = useContext(ThemeContext)26932694 useEffect(() => {2695 if (submitted) {2696 post('/api/register')2697 showToast('Registered', theme)2698 }2699 }, [submitted, theme])27002701 return <button onClick={() => setSubmitted(true)}>Submit</button>2702}2703```27042705**Correct (do it in the handler):**27062707```tsx2708function Form() {2709 const theme = useContext(ThemeContext)27102711 function handleSubmit() {2712 post('/api/register')2713 showToast('Registered', theme)2714 }27152716 return <button onClick={handleSubmit}>Submit</button>2717}2718```27192720Reference: [Should this code move to an event handler?](https://react.dev/learn/removing-effect-dependencies#should-this-code-move-to-an-event-handler)2721<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rerender-move-effect-to-event:end -->27222723<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rerender-no-inline-components:start -->2724## Don't Define Components Inside Components27252726**Impact: HIGH (prevents remount on every render)**27272728Defining a component inside another component creates a new component type on every render. React sees a different component each time and fully remounts it, destroying all state and DOM.27292730A common reason developers do this is to access parent variables without passing props. Always pass props instead.27312732**Incorrect (remounts on every render):**27332734```tsx2735function UserProfile({ user, theme }) {2736 // Defined inside to access `theme` - BAD2737 const Avatar = () => (2738 <img2739 src={user.avatarUrl}2740 className={theme === 'dark' ? 'avatar-dark' : 'avatar-light'}2741 />2742 )27432744 // Defined inside to access `user` - BAD2745 const Stats = () => (2746 <div>2747 <span>{user.followers} followers</span>2748 <span>{user.posts} posts</span>2749 </div>2750 )27512752 return (2753 <div>2754 <Avatar />2755 <Stats />2756 </div>2757 )2758}2759```27602761Every time `UserProfile` renders, `Avatar` and `Stats` are new component types. React unmounts the old instances and mounts new ones, losing any internal state, running effects again, and recreating DOM nodes.27622763**Correct (pass props instead):**27642765```tsx2766function Avatar({ src, theme }: { src: string; theme: string }) {2767 return (2768 <img2769 src={src}2770 className={theme === 'dark' ? 'avatar-dark' : 'avatar-light'}2771 />2772 )2773}27742775function Stats({ followers, posts }: { followers: number; posts: number }) {2776 return (2777 <div>2778 <span>{followers} followers</span>2779 <span>{posts} posts</span>2780 </div>2781 )2782}27832784function UserProfile({ user, theme }) {2785 return (2786 <div>2787 <Avatar src={user.avatarUrl} theme={theme} />2788 <Stats followers={user.followers} posts={user.posts} />2789 </div>2790 )2791}2792```27932794**Symptoms of this bug:**2795- Input fields lose focus on every keystroke2796- Animations restart unexpectedly2797- `useEffect` cleanup/setup runs on every parent render2798- Scroll position resets inside the component2799<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rerender-no-inline-components:end -->28002801<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rerender-simple-expression-in-memo:start -->2802## Do not wrap a simple expression with a primitive result type in useMemo28032804When an expression is simple (few logical or arithmetical operators) and has a primitive result type (boolean, number, string), do not wrap it in `useMemo`.2805Calling `useMemo` and comparing hook dependencies may consume more resources than the expression itself.28062807**Incorrect:**28082809```tsx2810function Header({ user, notifications }: Props) {2811 const isLoading = useMemo(() => {2812 return user.isLoading || notifications.isLoading2813 }, [user.isLoading, notifications.isLoading])28142815 if (isLoading) return <Skeleton />2816 // return some markup2817}2818```28192820**Correct:**28212822```tsx2823function Header({ user, notifications }: Props) {2824 const isLoading = user.isLoading || notifications.isLoading28252826 if (isLoading) return <Skeleton />2827 // return some markup2828}2829```2830<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rerender-simple-expression-in-memo:end -->28312832<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rerender-split-combined-hooks:start -->2833## Split Combined Hook Computations28342835When a hook contains multiple independent tasks with different dependencies, split them into separate hooks. A combined hook reruns all tasks when any dependency changes, even if some tasks don't use the changed value.28362837**Incorrect (changing `sortOrder` recomputes filtering):**28382839```tsx2840const sortedProducts = useMemo(() => {2841 const filtered = products.filter((p) => p.category === category)2842 const sorted = filtered.toSorted((a, b) =>2843 sortOrder === "asc" ? a.price - b.price : b.price - a.price2844 )2845 return sorted2846}, [products, category, sortOrder])2847```28482849**Correct (filtering only recomputes when products or category change):**28502851```tsx2852const filteredProducts = useMemo(2853 () => products.filter((p) => p.category === category),2854 [products, category]2855)28562857const sortedProducts = useMemo(2858 () =>2859 filteredProducts.toSorted((a, b) =>2860 sortOrder === "asc" ? a.price - b.price : b.price - a.price2861 ),2862 [filteredProducts, sortOrder]2863)2864```28652866This pattern also applies to `useEffect` when combining unrelated side effects:28672868**Incorrect (both effects run when either dependency changes):**28692870```tsx2871useEffect(() => {2872 analytics.trackPageView(pathname)2873 document.title = `${pageTitle} | My App`2874}, [pathname, pageTitle])2875```28762877**Correct (effects run independently):**28782879```tsx2880useEffect(() => {2881 analytics.trackPageView(pathname)2882}, [pathname])28832884useEffect(() => {2885 document.title = `${pageTitle} | My App`2886}, [pageTitle])2887```28882889**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, it automatically optimizes dependency tracking and may handle some of these cases for you.2890<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rerender-split-combined-hooks:end -->28912892<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rerender-transitions:start -->2893## Use Transitions for Non-Urgent Updates28942895Mark frequent, non-urgent state updates as transitions to maintain UI responsiveness.28962897**Incorrect (blocks UI on every scroll):**28982899```tsx2900function ScrollTracker() {2901 const [scrollY, setScrollY] = useState(0)2902 useEffect(() => {2903 const handler = () => setScrollY(window.scrollY)2904 window.addEventListener('scroll', handler, { passive: true })2905 return () => window.removeEventListener('scroll', handler)2906 }, [])2907}2908```29092910**Correct (non-blocking updates):**29112912```tsx2913import { startTransition } from 'react'29142915function ScrollTracker() {2916 const [scrollY, setScrollY] = useState(0)2917 useEffect(() => {2918 const handler = () => {2919 startTransition(() => setScrollY(window.scrollY))2920 }2921 window.addEventListener('scroll', handler, { passive: true })2922 return () => window.removeEventListener('scroll', handler)2923 }, [])2924}2925```2926<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rerender-transitions:end -->29272928<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rerender-use-deferred-value:start -->2929## Use useDeferredValue for Expensive Derived Renders29302931When user input triggers expensive computations or renders, use `useDeferredValue` to keep the input responsive. The deferred value lags behind, allowing React to prioritize the input update and render the expensive result when idle.29322933**Incorrect (input feels laggy while filtering):**29342935```tsx2936function Search({ items }: { items: Item[] }) {2937 const [query, setQuery] = useState('')2938 const filtered = items.filter(item => fuzzyMatch(item, query))29392940 return (2941 <>2942 <input value={query} onChange={e => setQuery(e.target.value)} />2943 <ResultsList results={filtered} />2944 </>2945 )2946}2947```29482949**Correct (input stays snappy, results render when ready):**29502951```tsx2952function Search({ items }: { items: Item[] }) {2953 const [query, setQuery] = useState('')2954 const deferredQuery = useDeferredValue(query)2955 const filtered = useMemo(2956 () => items.filter(item => fuzzyMatch(item, deferredQuery)),2957 [items, deferredQuery]2958 )2959 const isStale = query !== deferredQuery29602961 return (2962 <>2963 <input value={query} onChange={e => setQuery(e.target.value)} />2964 <div style={{ opacity: isStale ? 0.7 : 1 }}>2965 <ResultsList results={filtered} />2966 </div>2967 </>2968 )2969}2970```29712972**When to use:**29732974- Filtering/searching large lists2975- Expensive visualizations (charts, graphs) reacting to input2976- Any derived state that causes noticeable render delays29772978**Note:** Wrap the expensive computation in `useMemo` with the deferred value as a dependency, otherwise it still runs on every render.29792980Reference: [React useDeferredValue](https://react.dev/reference/react/useDeferredValue)2981<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rerender-use-deferred-value:end -->29822983<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rerender-use-ref-transient-values:start -->2984## Use useRef for Transient Values29852986When 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.29872988**Incorrect (renders every update):**29892990```tsx2991function Tracker() {2992 const [lastX, setLastX] = useState(0)29932994 useEffect(() => {2995 const onMove = (e: MouseEvent) => setLastX(e.clientX)2996 window.addEventListener('mousemove', onMove)2997 return () => window.removeEventListener('mousemove', onMove)2998 }, [])29993000 return (3001 <div3002 style={{3003 position: 'fixed',3004 top: 0,3005 left: lastX,3006 width: 8,3007 height: 8,3008 background: 'black',3009 }}3010 />3011 )3012}3013```30143015**Correct (no re-render for tracking):**30163017```tsx3018function Tracker() {3019 const lastXRef = useRef(0)3020 const dotRef = useRef<HTMLDivElement>(null)30213022 useEffect(() => {3023 const onMove = (e: MouseEvent) => {3024 lastXRef.current = e.clientX3025 const node = dotRef.current3026 if (node) {3027 node.style.transform = `translateX(${e.clientX}px)`3028 }3029 }3030 window.addEventListener('mousemove', onMove)3031 return () => window.removeEventListener('mousemove', onMove)3032 }, [])30333034 return (3035 <div3036 ref={dotRef}3037 style={{3038 position: 'fixed',3039 top: 0,3040 left: 0,3041 width: 8,3042 height: 8,3043 background: 'black',3044 transform: 'translateX(0px)',3045 }}3046 />3047 )3048}3049```3050<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:rerender-use-ref-transient-values:end -->30513052<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:server-after-nonblocking:start -->3053## Use after() for Non-Blocking Operations30543055Use 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.30563057**Incorrect (blocks response):**30583059```tsx3060import { logUserAction } from '@/app/utils'30613062export async function POST(request: Request) {3063 // Perform mutation3064 await updateDatabase(request)30653066 // Logging blocks the response3067 const userAgent = request.headers.get('user-agent') || 'unknown'3068 await logUserAction({ userAgent })30693070 return new Response(JSON.stringify({ status: 'success' }), {3071 status: 200,3072 headers: { 'Content-Type': 'application/json' }3073 })3074}3075```30763077**Correct (non-blocking):**30783079```tsx3080import { after } from 'next/server'3081import { headers, cookies } from 'next/headers'3082import { logUserAction } from '@/app/utils'30833084export async function POST(request: Request) {3085 // Perform mutation3086 await updateDatabase(request)30873088 // Log after response is sent3089 after(async () => {3090 const userAgent = (await headers()).get('user-agent') || 'unknown'3091 const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous'30923093 logUserAction({ sessionCookie, userAgent })3094 })30953096 return new Response(JSON.stringify({ status: 'success' }), {3097 status: 200,3098 headers: { 'Content-Type': 'application/json' }3099 })3100}3101```31023103The response is sent immediately while logging happens in the background.31043105**Common use cases:**31063107- Analytics tracking3108- Audit logging3109- Sending notifications3110- Cache invalidation3111- Cleanup tasks31123113**Important notes:**31143115- `after()` runs even if the response fails or redirects3116- Works in Server Actions, Route Handlers, and Server Components31173118Reference: [https://nextjs.org/docs/app/api-reference/functions/after](https://nextjs.org/docs/app/api-reference/functions/after)3119<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:server-after-nonblocking:end -->31203121<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:server-auth-actions:start -->3122## Authenticate Server Actions Like API Routes31233124**Impact: CRITICAL (prevents unauthorized access to server mutations)**31253126Server 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.31273128Next.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."31293130**Incorrect (no authentication check):**31313132```typescript3133'use server'31343135export async function deleteUser(userId: string) {3136 // Anyone can call this! No auth check3137 await db.user.delete({ where: { id: userId } })3138 return { success: true }3139}3140```31413142**Correct (authentication inside the action):**31433144```typescript3145'use server'31463147import { verifySession } from '@/lib/auth'3148import { unauthorized } from '@/lib/errors'31493150export async function deleteUser(userId: string) {3151 // Always check auth inside the action3152 const session = await verifySession()31533154 if (!session) {3155 throw unauthorized('Must be logged in')3156 }31573158 // Check authorization too3159 if (session.user.role !== 'admin' && session.user.id !== userId) {3160 throw unauthorized('Cannot delete other users')3161 }31623163 await db.user.delete({ where: { id: userId } })3164 return { success: true }3165}3166```31673168**With input validation:**31693170```typescript3171'use server'31723173import { verifySession } from '@/lib/auth'3174import { z } from 'zod'31753176const updateProfileSchema = z.object({3177 userId: z.string().uuid(),3178 name: z.string().min(1).max(100),3179 email: z.string().email()3180})31813182export async function updateProfile(data: unknown) {3183 // Validate input first3184 const validated = updateProfileSchema.parse(data)31853186 // Then authenticate3187 const session = await verifySession()3188 if (!session) {3189 throw new Error('Unauthorized')3190 }31913192 // Then authorize3193 if (session.user.id !== validated.userId) {3194 throw new Error('Can only update own profile')3195 }31963197 // Finally perform the mutation3198 await db.user.update({3199 where: { id: validated.userId },3200 data: {3201 name: validated.name,3202 email: validated.email3203 }3204 })32053206 return { success: true }3207}3208```32093210Reference: [https://nextjs.org/docs/app/guides/authentication](https://nextjs.org/docs/app/guides/authentication)3211<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:server-auth-actions:end -->32123213<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:server-cache-lru:start -->3214## Cross-Request LRU Caching32153216`React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache.32173218**Implementation:**32193220```typescript3221import { LRUCache } from 'lru-cache'32223223const cache = new LRUCache<string, any>({3224 max: 1000,3225 ttl: 5 * 60 * 1000 // 5 minutes3226})32273228export async function getUser(id: string) {3229 const cached = cache.get(id)3230 if (cached) return cached32313232 const user = await db.user.findUnique({ where: { id } })3233 cache.set(id, user)3234 return user3235}32363237// Request 1: DB query, result cached3238// Request 2: cache hit, no DB query3239```32403241Use when sequential user actions hit multiple endpoints needing the same data within seconds.32423243**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.32443245**In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching.32463247Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)3248<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:server-cache-lru:end -->32493250<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:server-cache-react:start -->3251## Per-Request Deduplication with React.cache()32523253Use `React.cache()` for server-side request deduplication. Authentication and database queries benefit most.32543255**Usage:**32563257```typescript3258import { cache } from 'react'32593260export const getCurrentUser = cache(async () => {3261 const session = await auth()3262 if (!session?.user?.id) return null3263 return await db.user.findUnique({3264 where: { id: session.user.id }3265 })3266})3267```32683269Within a single request, multiple calls to `getCurrentUser()` execute the query only once.32703271**Avoid inline objects as arguments:**32723273`React.cache()` uses shallow equality (`Object.is`) to determine cache hits. Inline objects create new references each call, preventing cache hits.32743275**Incorrect (always cache miss):**32763277```typescript3278const getUser = cache(async (params: { uid: number }) => {3279 return await db.user.findUnique({ where: { id: params.uid } })3280})32813282// Each call creates new object, never hits cache3283getUser({ uid: 1 })3284getUser({ uid: 1 }) // Cache miss, runs query again3285```32863287**Correct (cache hit):**32883289```typescript3290const getUser = cache(async (uid: number) => {3291 return await db.user.findUnique({ where: { id: uid } })3292})32933294// Primitive args use value equality3295getUser(1)3296getUser(1) // Cache hit, returns cached result3297```32983299If you must pass objects, pass the same reference:33003301```typescript3302const params = { uid: 1 }3303getUser(params) // Query runs3304getUser(params) // Cache hit (same reference)3305```33063307**Next.js-Specific Note:**33083309In 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:33103311- Database queries (Prisma, Drizzle, etc.)3312- Heavy computations3313- Authentication checks3314- File system operations3315- Any non-fetch async work33163317Use `React.cache()` to deduplicate these operations across your component tree.33183319Reference: [React.cache documentation](https://react.dev/reference/react/cache)3320<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:server-cache-react:end -->33213322<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:server-dedup-props:start -->3323## Avoid Duplicate Serialization in RSC Props33243325**Impact: LOW (reduces network payload by avoiding duplicate serialization)**33263327RSC→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.33283329**Incorrect (duplicates array):**33303331```tsx3332// RSC: sends 6 strings (2 arrays × 3 items)3333<ClientList usernames={usernames} usernamesOrdered={usernames.toSorted()} />3334```33353336**Correct (sends 3 strings):**33373338```tsx3339// RSC: send once3340<ClientList usernames={usernames} />33413342// Client: transform there3343'use client'3344const sorted = useMemo(() => [...usernames].sort(), [usernames])3345```33463347**Nested deduplication behavior:**33483349Deduplication works recursively. Impact varies by data type:33503351- `string[]`, `number[]`, `boolean[]`: **HIGH impact** - array + all primitives fully duplicated3352- `object[]`: **LOW impact** - array duplicated, but nested objects deduplicated by reference33533354```tsx3355// string[] - duplicates everything3356usernames={['a','b']} sorted={usernames.toSorted()} // sends 4 strings33573358// object[] - duplicates array structure only3359users={[{id:1},{id:2}]} sorted={users.toSorted()} // sends 2 arrays + 2 unique objects (not 4)3360```33613362**Operations breaking deduplication (create new references):**33633364- Arrays: `.toSorted()`, `.filter()`, `.map()`, `.slice()`, `[...arr]`3365- Objects: `{...obj}`, `Object.assign()`, `structuredClone()`, `JSON.parse(JSON.stringify())`33663367**More examples:**33683369```tsx3370// ❌ Bad3371<C users={users} active={users.filter(u => u.active)} />3372<C product={product} productName={product.name} />33733374// ✅ Good3375<C users={users} />3376<C product={product} />3377// Do filtering/destructuring in client3378```33793380**Exception:** Pass derived data when transformation is expensive or client doesn't need original.3381<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:server-dedup-props:end -->33823383<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:server-hoist-static-io:start -->3384## Hoist Static I/O to Module Level33853386**Impact: HIGH (avoids repeated file/network I/O per request)**33873388When loading static assets (fonts, logos, images, config files) in route handlers or server functions, hoist the I/O operation to module level. Module-level code runs once when the module is first imported, not on every request. This eliminates redundant file system reads or network fetches that would otherwise run on every invocation.33893390**Incorrect (reads font file on every request):**33913392```typescript3393// app/api/og/route.tsx3394import { ImageResponse } from 'next/og'33953396export async function GET(request: Request) {3397 // Runs on EVERY request - expensive!3398 const fontData = await fetch(3399 new URL('./fonts/Inter.ttf', import.meta.url)3400 ).then(res => res.arrayBuffer())34013402 const logoData = await fetch(3403 new URL('./images/logo.png', import.meta.url)3404 ).then(res => res.arrayBuffer())34053406 return new ImageResponse(3407 <div style={{ fontFamily: 'Inter' }}>3408 <img src={logoData} />3409 Hello World3410 </div>,3411 { fonts: [{ name: 'Inter', data: fontData }] }3412 )3413}3414```34153416**Correct (loads once at module initialization):**34173418```typescript3419// app/api/og/route.tsx3420import { ImageResponse } from 'next/og'34213422// Module-level: runs ONCE when module is first imported3423const fontData = fetch(3424 new URL('./fonts/Inter.ttf', import.meta.url)3425).then(res => res.arrayBuffer())34263427const logoData = fetch(3428 new URL('./images/logo.png', import.meta.url)3429).then(res => res.arrayBuffer())34303431export async function GET(request: Request) {3432 // Await the already-started promises3433 const [font, logo] = await Promise.all([fontData, logoData])34343435 return new ImageResponse(3436 <div style={{ fontFamily: 'Inter' }}>3437 <img src={logo} />3438 Hello World3439 </div>,3440 { fonts: [{ name: 'Inter', data: font }] }3441 )3442}3443```34443445**Correct (synchronous fs at module level):**34463447```typescript3448// app/api/og/route.tsx3449import { ImageResponse } from 'next/og'3450import { readFileSync } from 'fs'3451import { join } from 'path'34523453// Synchronous read at module level - blocks only during module init3454const fontData = readFileSync(3455 join(process.cwd(), 'public/fonts/Inter.ttf')3456)34573458const logoData = readFileSync(3459 join(process.cwd(), 'public/images/logo.png')3460)34613462export async function GET(request: Request) {3463 return new ImageResponse(3464 <div style={{ fontFamily: 'Inter' }}>3465 <img src={logoData} />3466 Hello World3467 </div>,3468 { fonts: [{ name: 'Inter', data: fontData }] }3469 )3470}3471```34723473**Incorrect (reads config on every call):**34743475```typescript3476import fs from 'node:fs/promises'34773478export async function processRequest(data: Data) {3479 const config = JSON.parse(3480 await fs.readFile('./config.json', 'utf-8')3481 )3482 const template = await fs.readFile('./template.html', 'utf-8')34833484 return render(template, data, config)3485}3486```34873488**Correct (hoists config and template to module level):**34893490```typescript3491import fs from 'node:fs/promises'34923493const configPromise = fs3494 .readFile('./config.json', 'utf-8')3495 .then(JSON.parse)3496const templatePromise = fs.readFile('./template.html', 'utf-8')34973498export async function processRequest(data: Data) {3499 const [config, template] = await Promise.all([3500 configPromise,3501 templatePromise,3502 ])35033504 return render(template, data, config)3505}3506```35073508When to use this pattern:35093510- Loading fonts for OG image generation3511- Loading static logos, icons, or watermarks3512- Reading configuration files that don't change at runtime3513- Loading email templates or other static templates3514- Any static asset that's the same across all requests35153516When not to use this pattern:35173518- Assets that vary per request or user3519- Files that may change during runtime (use caching with TTL instead)3520- Large files that would consume too much memory if kept loaded3521- Sensitive data that shouldn't persist in memory35223523With Vercel's [Fluid Compute](https://vercel.com/docs/fluid-compute), module-level caching is especially effective because multiple concurrent requests share the same function instance. The static assets stay loaded in memory across requests without cold start penalties.35243525In traditional serverless, each cold start re-executes module-level code, but subsequent warm invocations reuse the loaded assets until the instance is recycled.3526<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:server-hoist-static-io:end -->35273528<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:server-no-shared-module-state:start -->3529## Avoid Shared Module State for Request Data35303531For React Server Components and client components rendered during SSR, avoid using mutable module-level variables to share request-scoped data. Server renders can run concurrently in the same process. If one render writes to shared module state and another render reads it, you can get race conditions, cross-request contamination, and security bugs where one user's data appears in another user's response.35323533Treat module scope on the server as process-wide shared memory, not request-local state.35343535**Incorrect (request data leaks across concurrent renders):**35363537```tsx3538let currentUser: User | null = null35393540export default async function Page() {3541 currentUser = await auth()3542 return <Dashboard />3543}35443545async function Dashboard() {3546 return <div>{currentUser?.name}</div>3547}3548```35493550If two requests overlap, request A can set `currentUser`, then request B overwrites it before request A finishes rendering `Dashboard`.35513552**Correct (keep request data local to the render tree):**35533554```tsx3555export default async function Page() {3556 const user = await auth()3557 return <Dashboard user={user} />3558}35593560function Dashboard({ user }: { user: User | null }) {3561 return <div>{user?.name}</div>3562}3563```35643565Safe exceptions:35663567- Immutable static assets or config loaded once at module scope3568- Shared caches intentionally designed for cross-request reuse and keyed correctly3569- Process-wide singletons that do not store request- or user-specific mutable data35703571For static assets and config, see [Hoist Static I/O to Module Level](./server-hoist-static-io.md).3572<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:server-no-shared-module-state:end -->35733574<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:server-parallel-fetching:start -->3575## Parallel Data Fetching with Component Composition35763577React Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching.35783579**Incorrect (Sidebar waits for Page's fetch to complete):**35803581```tsx3582export default async function Page() {3583 const header = await fetchHeader()3584 return (3585 <div>3586 <div>{header}</div>3587 <Sidebar />3588 </div>3589 )3590}35913592async function Sidebar() {3593 const items = await fetchSidebarItems()3594 return <nav>{items.map(renderItem)}</nav>3595}3596```35973598**Correct (both fetch simultaneously):**35993600```tsx3601async function Header() {3602 const data = await fetchHeader()3603 return <div>{data}</div>3604}36053606async function Sidebar() {3607 const items = await fetchSidebarItems()3608 return <nav>{items.map(renderItem)}</nav>3609}36103611export default function Page() {3612 return (3613 <div>3614 <Header />3615 <Sidebar />3616 </div>3617 )3618}3619```36203621**Alternative with children prop:**36223623```tsx3624async function Header() {3625 const data = await fetchHeader()3626 return <div>{data}</div>3627}36283629async function Sidebar() {3630 const items = await fetchSidebarItems()3631 return <nav>{items.map(renderItem)}</nav>3632}36333634function Layout({ children }: { children: ReactNode }) {3635 return (3636 <div>3637 <Header />3638 {children}3639 </div>3640 )3641}36423643export default function Page() {3644 return (3645 <Layout>3646 <Sidebar />3647 </Layout>3648 )3649}3650```3651<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:server-parallel-fetching:end -->36523653<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:server-parallel-nested-fetching:start -->3654## Parallel Nested Data Fetching36553656When fetching nested data in parallel, chain dependent fetches within each item's promise so a slow item doesn't block the rest.36573658**Incorrect (a single slow item blocks all nested fetches):**36593660```tsx3661const chats = await Promise.all(3662 chatIds.map(id => getChat(id))3663)36643665const chatAuthors = await Promise.all(3666 chats.map(chat => getUser(chat.author))3667)3668```36693670If one `getChat(id)` out of 100 is extremely slow, the authors of the other 99 chats can't start loading even though their data is ready.36713672**Correct (each item chains its own nested fetch):**36733674```tsx3675const chatAuthors = await Promise.all(3676 chatIds.map(id => getChat(id).then(chat => getUser(chat.author)))3677)3678```36793680Each item independently chains `getChat` → `getUser`, so a slow chat doesn't block author fetches for the others.3681<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:server-parallel-nested-fetching:end -->36823683<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:server-serialization:start -->3684## Minimize Serialization at RSC Boundaries36853686The 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.36873688**Incorrect (serializes all 50 fields):**36893690```tsx3691async function Page() {3692 const user = await fetchUser() // 50 fields3693 return <Profile user={user} />3694}36953696'use client'3697function Profile({ user }: { user: User }) {3698 return <div>{user.name}</div> // uses 1 field3699}3700```37013702**Correct (serializes only 1 field):**37033704```tsx3705async function Page() {3706 const user = await fetchUser()3707 return <Profile name={user.name} />3708}37093710'use client'3711function Profile({ name }: { name: string }) {3712 return <div>{name}</div>3713}3714```3715<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-best-practices:server-serialization:end -->3716
One repository carrying more than one format is the comparison this product exists for: does anyone actually write different content in each file, or is one a copy of the other?
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| nota-america/forgecat-agent-profilesprofiles/addyosmani/agent-skills/for-cursor/.cursor/rules/cmd-build.mdc · 51 | Cursor rules | no sections | 16/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/addyosmani/agent-skills/for-cursor/.cursor/rules/cmd-code-simplify.mdc · 51 | Cursor rules | testing-strategy | 30/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/addyosmani/agent-skills/for-cursor/.cursor/rules/cmd-plan.mdc · 51 | Cursor rules | no sections | 16/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/addyosmani/agent-skills/for-cursor/.cursor/rules/cmd-review.mdc · 51 | Cursor rules | no sections | 16/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/addyosmani/agent-skills/for-cursor/.cursor/rules/cmd-ship.mdc · 51 | Cursor rules | testing-strategygitdeploymentdo-not | 61/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/addyosmani/agent-skills/for-cursor/.cursor/rules/cmd-spec.mdc · 51 | Cursor rules | no sections | 16/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/addyosmani/agent-skills/for-cursor/.cursor/rules/cmd-test.mdc · 51 | Cursor rules | no sections | 16/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/addyosmani/agent-skills/for-forgecat/AGENTS.md · 51 | AGENTS.md | lint-formatstylearchdo-not | 73/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/addyosmani/agent-skills/for-forgecat/CLAUDE.md · 51 | CLAUDE.md | teststylearchagent-behaviour | 70/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/anthropics/claude-code/anthropics_claude-code_ralph-wiggum/for-cursor/.cursor/rules/cmd-cancel-ralph.mdc · 51 | Cursor rules | no sections | 16/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/anthropics/claude-code/anthropics_claude-code_ralph-wiggum/for-cursor/.cursor/rules/cmd-help.mdc · 51 | Cursor rules | no sections | 54/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/anthropics/claude-code/anthropics_claude-code_ralph-wiggum/for-cursor/.cursor/rules/cmd-ralph-loop.mdc · 51 | Cursor rules | no sections | 22/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/anthropics/claude-plugins-official/anthropics_claude-plugins-official_agent-sdk-dev/for-cursor/.cursor/rules/cmd-new-sdk-app.mdc · 51 | Cursor rules | setupstylearchdocs | 76/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/anthropics/claude-plugins-official/anthropics_claude-plugins-official_claude-md-management/for-cursor/.cursor/rules/cmd-revise-claude-md.mdc · 51 | Cursor rules | agent-behaviour | 50/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/anthropics/claude-plugins-official/anthropics_claude-plugins-official_code-review/for-cursor/.cursor/rules/cmd-code-review.mdc · 51 | Cursor rules | testing-strategygit | 35/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/anthropics/claude-plugins-official/anthropics_claude-plugins-official_commit-commands/for-cursor/.cursor/rules/cmd-clean_gone.mdc · 51 | Cursor rules | no sections | 60/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/anthropics/claude-plugins-official/anthropics_claude-plugins-official_commit-commands/for-cursor/.cursor/rules/cmd-commit-push-pr.mdc · 51 | Cursor rules | stylegit | 44/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/anthropics/claude-plugins-official/anthropics_claude-plugins-official_commit-commands/for-cursor/.cursor/rules/cmd-commit.mdc · 51 | Cursor rules | style | 44/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/anthropics/claude-plugins-official/anthropics_claude-plugins-official_example-plugin/for-cursor/.cursor/rules/cmd-example-command.mdc · 51 | Cursor rules | lint-formatstyleagent-behaviour | 58/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/anthropics/claude-plugins-official/anthropics_claude-plugins-official_feature-dev/for-cursor/.cursor/rules/cmd-feature-dev.mdc · 51 | Cursor rules | stylearchgit | 56/100 | today |
Same format, overlapping stack, ranked by quality.
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| vllm-project/vllmAGENTS.md · 89k | AGENTS.md | setuptestlint-formatstyle+5 | 100/100 | 14 days ago | |
| code-yeongyu/oh-my-openagentpackages/web/AGENTS.md · 68k | AGENTS.md | setupbuildtestlint-format+6 | 100/100 | 13 days ago | |
| deepseek-ai/deepseek-harnessnative/landlock-run/AGENTS.md · 104k | AGENTS.md | setupteststylearch+3 | 100/100 | today | |
| TryGhost/Ghoste2e/AGENTS.md · 55k | AGENTS.md | setupteststylearch+2 | 100/100 | today | |
| aaif-goose/gooseAGENTS.md · 53k | AGENTS.md | setupbuildtestlint-format+7 | 100/100 | 8 days ago | |
| duckduckgo/content-scope-scriptsspecial-pages/AGENTS.md · 70 | AGENTS.md | buildteststylearch+3 | 100/100 | 14 days ago | |
| n8n-io/n8npackages/@n8n/agents/AGENTS.md · 201k | AGENTS.md | buildteststylearch+3 | 100/100 | 14 days ago | |
| mui/material-uiAGENTS.md · 99k | AGENTS.md | setupbuildtestlint-format+9 | 100/100 | 14 days ago |
A badge carrying the measured quality of the strongest agent config file in this repository, out of 100. It reads from this index every time somebody loads your page, so it changes when the measurement changes and there is nothing to keep up to date. Free, no account, and the value is not something you or we can set by hand.
[](https://rulestack.kynth.studio/configs/nota-america-forgecat-agent-profiles-profiles-vercel-labs-agent-skills-vercel-labs-agent-skills-react-best-practices-for-codex-agents)Would rather not hotlink us? Every badge is also served in shields.io’s endpoint schema, so shields renders the image and your readers never talk to our domain:
Published by Toolproof, the masthead over this index and eight others. The method behind the number is at toolproof.kynth.studio/methodology, and the whole thing is readable as JSON with no key at /api.