From 331a2596fa90f3f0eb6e1530541b1719acc10ba7 Mon Sep 17 00:00:00 2001 From: kenneth Date: Sat, 17 Jan 2026 11:53:08 +0000 Subject: [PATCH] Add Claude skills for React best practices and web design - react-best-practices: Performance optimization patterns (client-side only) - web-design-guidelines: UI review against Web Interface Guidelines Co-authored-by: Ona --- .../vercel-react-best-practices/AGENTS.md | 807 ++++++++++++++++++ .../vercel-react-best-practices/SKILL.md | 111 +++ .../rules/advanced-event-handler-refs.md | 55 ++ .../rules/advanced-use-latest.md | 49 ++ .../rules/async-defer-await.md | 80 ++ .../rules/async-dependencies.md | 36 + .../rules/async-parallel.md | 28 + .../rules/bundle-barrel-imports.md | 59 ++ .../rules/bundle-conditional.md | 31 + .../rules/bundle-preload.md | 50 ++ .../rules/client-event-listeners.md | 74 ++ .../rules/client-localstorage-schema.md | 71 ++ .../rules/client-passive-event-listeners.md | 48 ++ .../rules/client-swr-dedup.md | 56 ++ .../rules/js-batch-dom-css.md | 82 ++ .../rules/js-cache-function-results.md | 80 ++ .../rules/js-cache-property-access.md | 28 + .../rules/js-cache-storage.md | 70 ++ .../rules/js-combine-iterations.md | 32 + .../rules/js-early-exit.md | 50 ++ .../rules/js-hoist-regexp.md | 45 + .../rules/js-index-maps.md | 37 + .../rules/js-length-check-first.md | 49 ++ .../rules/js-min-max-loop.md | 82 ++ .../rules/js-set-map-lookups.md | 24 + .../rules/js-tosorted-immutable.md | 57 ++ .../rules/rendering-animate-svg-wrapper.md | 47 + .../rules/rendering-conditional-render.md | 40 + .../rules/rendering-content-visibility.md | 38 + .../rules/rendering-hoist-jsx.md | 46 + .../rules/rendering-svg-precision.md | 28 + .../rules/rerender-defer-reads.md | 39 + .../rules/rerender-dependencies.md | 45 + .../rules/rerender-derived-state.md | 29 + .../rules/rerender-functional-setstate.md | 74 ++ .../rules/rerender-lazy-state-init.md | 58 ++ .../rules/rerender-memo.md | 44 + .../rules/rerender-transitions.md | 40 + .claude/skills/web-design-guidelines/SKILL.md | 39 + 39 files changed, 2758 insertions(+) create mode 100644 .claude/skills/vercel-react-best-practices/AGENTS.md create mode 100644 .claude/skills/vercel-react-best-practices/SKILL.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/advanced-use-latest.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/async-defer-await.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/async-dependencies.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/async-parallel.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/bundle-conditional.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/bundle-preload.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/client-event-listeners.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/client-localstorage-schema.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/client-swr-dedup.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/js-batch-dom-css.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/js-cache-function-results.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/js-cache-property-access.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/js-cache-storage.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/js-combine-iterations.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/js-early-exit.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/js-hoist-regexp.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/js-index-maps.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/js-length-check-first.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/js-min-max-loop.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/js-set-map-lookups.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/rendering-conditional-render.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/rendering-content-visibility.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/rendering-svg-precision.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/rerender-defer-reads.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/rerender-dependencies.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/rerender-derived-state.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/rerender-memo.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/rerender-transitions.md create mode 100644 .claude/skills/web-design-guidelines/SKILL.md diff --git a/.claude/skills/vercel-react-best-practices/AGENTS.md b/.claude/skills/vercel-react-best-practices/AGENTS.md new file mode 100644 index 0000000..3deafbd --- /dev/null +++ b/.claude/skills/vercel-react-best-practices/AGENTS.md @@ -0,0 +1,807 @@ +# React Best Practices + +**Version 1.0.0** + +> **Note:** +> This document is for agents and LLMs to follow when maintaining, +> generating, or refactoring React codebases. Humans may also find it useful. + +--- + +## Abstract + +Performance optimization guide for React applications, designed for AI agents and LLMs. Contains rules across 7 categories, prioritized by impact from critical (eliminating waterfalls, reducing bundle size) to incremental (advanced patterns). + +--- + +## Table of Contents + +1. [Eliminating Waterfalls](#1-eliminating-waterfalls) — **CRITICAL** +2. [Bundle Size Optimization](#2-bundle-size-optimization) — **CRITICAL** +3. [Client-Side Data Fetching](#3-client-side-data-fetching) — **MEDIUM-HIGH** +4. [Re-render Optimization](#4-re-render-optimization) — **MEDIUM** +5. [Rendering Performance](#5-rendering-performance) — **MEDIUM** +6. [JavaScript Performance](#6-javascript-performance) — **LOW-MEDIUM** +7. [Advanced Patterns](#7-advanced-patterns) — **LOW** + +--- + +## 1. Eliminating Waterfalls + +**Impact: CRITICAL** + +Waterfalls are the #1 performance killer. Each sequential await adds full network latency. + +### 1.1 Defer Await Until Needed + +**Impact: HIGH (avoids blocking unused code paths)** + +Move `await` operations into the branches where they're actually used. + +**Incorrect: blocks both branches** + +```typescript +async function handleRequest(userId: string, skipProcessing: boolean) { + const userData = await fetchUserData(userId) + + if (skipProcessing) { + return { skipped: true } + } + + return processUserData(userData) +} +``` + +**Correct: only blocks when needed** + +```typescript +async function handleRequest(userId: string, skipProcessing: boolean) { + if (skipProcessing) { + return { skipped: true } + } + + const userData = await fetchUserData(userId) + return processUserData(userData) +} +``` + +### 1.2 Dependency-Based Parallelization + +**Impact: CRITICAL (2-10× improvement)** + +For operations with partial dependencies, use `better-all` to maximize parallelism. + +**Incorrect: profile waits for config unnecessarily** + +```typescript +const [user, config] = await Promise.all([ + fetchUser(), + fetchConfig() +]) +const profile = await fetchProfile(user.id) +``` + +**Correct: config and profile run in parallel** + +```typescript +import { all } from 'better-all' + +const { user, config, profile } = await all({ + async user() { return fetchUser() }, + async config() { return fetchConfig() }, + async profile() { + return fetchProfile((await this.$.user).id) + } +}) +``` + +### 1.3 Promise.all() for Independent Operations + +**Impact: CRITICAL (2-10× improvement)** + +When async operations have no interdependencies, execute them concurrently. + +**Incorrect: sequential execution, 3 round trips** + +```typescript +const user = await fetchUser() +const posts = await fetchPosts() +const comments = await fetchComments() +``` + +**Correct: parallel execution, 1 round trip** + +```typescript +const [user, posts, comments] = await Promise.all([ + fetchUser(), + fetchPosts(), + fetchComments() +]) +``` + +--- + +## 2. Bundle Size Optimization + +**Impact: CRITICAL** + +Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint. + +### 2.1 Avoid Barrel File Imports + +**Impact: CRITICAL (200-800ms import cost, slow builds)** + +Import directly from source files instead of barrel files. + +**Incorrect: imports entire library** + +```tsx +import { Check, X, Menu } from 'lucide-react' +// Loads 1,583 modules +``` + +**Correct: imports only what you need** + +```tsx +import Check from 'lucide-react/dist/esm/icons/check' +import X from 'lucide-react/dist/esm/icons/x' +import Menu from 'lucide-react/dist/esm/icons/menu' +// Loads only 3 modules +``` + +Libraries commonly affected: `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `@headlessui/react`, `@radix-ui/react-*`, `lodash`, `ramda`, `date-fns`, `rxjs`, `react-use`. + +### 2.2 Conditional Module Loading + +**Impact: HIGH (loads large data only when needed)** + +Load large data or modules only when a feature is activated. + +```tsx +function AnimationPlayer({ enabled, setEnabled }) { + const [frames, setFrames] = useState(null) + + useEffect(() => { + if (enabled && !frames) { + import('./animation-frames.js') + .then(mod => setFrames(mod.frames)) + .catch(() => setEnabled(false)) + } + }, [enabled, frames, setEnabled]) + + if (!frames) return + return +} +``` + +### 2.3 Preload Based on User Intent + +**Impact: MEDIUM (reduces perceived latency)** + +Preload heavy bundles before they're needed. + +```tsx +function EditorButton({ onClick }) { + const preload = () => { + void import('./monaco-editor') + } + + return ( + + ) +} +``` + +--- + +## 3. Client-Side Data Fetching + +**Impact: MEDIUM-HIGH** + +### 3.1 Deduplicate Global Event Listeners + +**Impact: MEDIUM (prevents memory leaks and duplicate handlers)** + +Use a singleton pattern for global event listeners. + +**Incorrect: multiple listeners** + +```tsx +function useWindowResize(callback) { + useEffect(() => { + window.addEventListener('resize', callback) + return () => window.removeEventListener('resize', callback) + }, [callback]) +} +``` + +**Correct: shared listener** + +```tsx +const listeners = new Set() +let isListening = false + +function useWindowResize(callback) { + useEffect(() => { + listeners.add(callback) + + if (!isListening) { + isListening = true + window.addEventListener('resize', (e) => { + listeners.forEach(fn => fn(e)) + }) + } + + return () => listeners.delete(callback) + }, [callback]) +} +``` + +### 3.2 Use Passive Event Listeners + +**Impact: MEDIUM (improves scroll performance)** + +Use passive listeners for scroll and touch events. + +```tsx +useEffect(() => { + const handler = (e) => { /* handle scroll */ } + window.addEventListener('scroll', handler, { passive: true }) + return () => window.removeEventListener('scroll', handler) +}, []) +``` + +### 3.3 Use SWR for Automatic Deduplication + +**Impact: HIGH (eliminates duplicate requests)** + +SWR automatically deduplicates requests to the same key. + +```tsx +import useSWR from 'swr' + +function UserProfile({ userId }) { + const { data } = useSWR(`/api/users/${userId}`, fetcher) + return
{data?.name}
+} + +// Multiple components using the same key = single request +``` + +### 3.4 Version and Minimize localStorage Data + +**Impact: MEDIUM (prevents data corruption)** + +Use schema versioning for localStorage. + +```typescript +const STORAGE_VERSION = 2 + +interface StoredData { + version: number + data: UserPreferences +} + +function loadPreferences(): UserPreferences { + const raw = localStorage.getItem('prefs') + if (!raw) return defaultPreferences + + const stored: StoredData = JSON.parse(raw) + if (stored.version !== STORAGE_VERSION) { + return migrate(stored) + } + return stored.data +} +``` + +--- + +## 4. Re-render Optimization + +**Impact: MEDIUM** + +### 4.1 Defer State Reads to Usage Point + +**Impact: MEDIUM (avoids unnecessary re-renders)** + +Don't subscribe to state that's only used in callbacks. + +**Incorrect: re-renders on every count change** + +```tsx +function Counter() { + const count = useStore(state => state.count) + const increment = useStore(state => state.increment) + + return +} +``` + +**Correct: no re-renders from count changes** + +```tsx +function Counter() { + const increment = useStore(state => state.increment) + + return +} +``` + +### 4.2 Extract to Memoized Components + +**Impact: MEDIUM (isolates expensive renders)** + +Extract expensive computations into memoized child components. + +```tsx +const ExpensiveList = memo(function ExpensiveList({ items }) { + return items.map(item => ) +}) + +function Parent() { + const [filter, setFilter] = useState('') + const items = useItems() + + return ( + <> + setFilter(e.target.value)} /> + + + ) +} +``` + +### 4.3 Narrow Effect Dependencies + +**Impact: MEDIUM (reduces effect runs)** + +Use primitive dependencies instead of objects. + +**Incorrect: runs on every render** + +```tsx +useEffect(() => { + fetchData(options) +}, [options]) // Object reference changes every render +``` + +**Correct: runs only when values change** + +```tsx +useEffect(() => { + fetchData({ page, limit }) +}, [page, limit]) +``` + +### 4.4 Subscribe to Derived State + +**Impact: MEDIUM (reduces re-renders)** + +Subscribe to derived booleans instead of raw values. + +**Incorrect: re-renders on every count change** + +```tsx +function Badge() { + const count = useStore(state => state.notifications.length) + return count > 0 ? New : null +} +``` + +**Correct: re-renders only when visibility changes** + +```tsx +function Badge() { + const hasNotifications = useStore(state => state.notifications.length > 0) + return hasNotifications ? New : null +} +``` + +### 4.5 Use Functional setState Updates + +**Impact: MEDIUM (stable callback references)** + +Use functional updates to avoid dependency on current state. + +```tsx +// Incorrect: callback changes when count changes +const increment = useCallback(() => { + setCount(count + 1) +}, [count]) + +// Correct: stable callback reference +const increment = useCallback(() => { + setCount(c => c + 1) +}, []) +``` + +### 4.6 Use Lazy State Initialization + +**Impact: LOW-MEDIUM (avoids expensive initial computation)** + +Pass a function to useState for expensive initial values. + +```tsx +// Incorrect: parses on every render +const [data, setData] = useState(JSON.parse(localStorage.getItem('data'))) + +// Correct: parses only once +const [data, setData] = useState(() => JSON.parse(localStorage.getItem('data'))) +``` + +### 4.7 Use Transitions for Non-Urgent Updates + +**Impact: MEDIUM (keeps UI responsive)** + +Use startTransition for updates that can be deferred. + +```tsx +import { startTransition } from 'react' + +function SearchResults() { + const [query, setQuery] = useState('') + const [results, setResults] = useState([]) + + const handleChange = (e) => { + setQuery(e.target.value) // Urgent: update input immediately + + startTransition(() => { + setResults(search(e.target.value)) // Non-urgent: can be interrupted + }) + } + + return ( + <> + + + + ) +} +``` + +--- + +## 5. Rendering Performance + +**Impact: MEDIUM** + +### 5.1 Animate SVG Wrapper Instead of SVG Element + +**Impact: MEDIUM (avoids SVG re-parsing)** + +Wrap SVGs in a div and animate the wrapper. + +```tsx +// Incorrect: triggers SVG re-parsing +... + +// Correct: animates wrapper only + + ... + +``` + +### 5.2 CSS content-visibility for Long Lists + +**Impact: HIGH (skips off-screen rendering)** + +Use content-visibility to skip rendering off-screen items. + +```css +.list-item { + content-visibility: auto; + contain-intrinsic-size: 0 50px; +} +``` + +### 5.3 Hoist Static JSX Elements + +**Impact: LOW-MEDIUM (avoids recreation)** + +Extract static JSX outside components. + +```tsx +const staticIcon = + +function ListItem({ label }) { + return ( +
+ {staticIcon} + {label} +
+ ) +} +``` + +### 5.4 Optimize SVG Precision + +**Impact: LOW (reduces SVG size)** + +Reduce coordinate precision in SVGs. + +```tsx +// Before: 847 bytes + + +// After: 324 bytes + +``` + +### 5.5 Use Explicit Conditional Rendering + +**Impact: LOW (prevents rendering bugs)** + +Use ternary instead of && for conditionals. + +```tsx +// Incorrect: renders "0" when count is 0 +{count && } + +// Correct: renders nothing when count is 0 +{count > 0 ? : null} +``` + +--- + +## 6. JavaScript Performance + +**Impact: LOW-MEDIUM** + +### 6.1 Batch DOM CSS Changes + +**Impact: MEDIUM (reduces reflows)** + +Group CSS changes via classes or cssText. + +```typescript +// Incorrect: 3 reflows +element.style.width = '100px' +element.style.height = '100px' +element.style.margin = '10px' + +// Correct: 1 reflow +element.style.cssText = 'width: 100px; height: 100px; margin: 10px;' +``` + +### 6.2 Build Index Maps for Repeated Lookups + +**Impact: HIGH for large datasets** + +Build a Map for O(1) lookups instead of O(n) array searches. + +```typescript +// Incorrect: O(n) for each lookup +users.find(u => u.id === targetId) + +// Correct: O(1) lookup +const userMap = new Map(users.map(u => [u.id, u])) +userMap.get(targetId) +``` + +### 6.3 Cache Property Access in Loops + +**Impact: LOW-MEDIUM** + +Cache object properties accessed in loops. + +```typescript +// Incorrect +for (let i = 0; i < items.length; i++) { + process(items[i]) +} + +// Correct +const len = items.length +for (let i = 0; i < len; i++) { + process(items[i]) +} +``` + +### 6.4 Cache Repeated Function Calls + +**Impact: MEDIUM** + +Cache expensive function results. + +```typescript +const cache = new Map() + +function expensiveComputation(input) { + if (cache.has(input)) return cache.get(input) + const result = /* expensive work */ + cache.set(input, result) + return result +} +``` + +### 6.5 Cache Storage API Calls + +**Impact: MEDIUM** + +Cache localStorage/sessionStorage reads. + +```typescript +let cachedTheme = null + +function getTheme() { + if (cachedTheme === null) { + cachedTheme = localStorage.getItem('theme') || 'light' + } + return cachedTheme +} +``` + +### 6.6 Combine Multiple Array Iterations + +**Impact: LOW-MEDIUM** + +Combine filter/map into a single loop. + +```typescript +// Incorrect: 2 iterations +const result = items + .filter(item => item.active) + .map(item => item.value) + +// Correct: 1 iteration +const result = [] +for (const item of items) { + if (item.active) result.push(item.value) +} +``` + +### 6.7 Early Length Check for Array Comparisons + +**Impact: LOW** + +Check array length before expensive comparisons. + +```typescript +function arraysEqual(a, b) { + if (a.length !== b.length) return false + return a.every((val, i) => val === b[i]) +} +``` + +### 6.8 Early Return from Functions + +**Impact: LOW** + +Return early to avoid unnecessary work. + +```typescript +function processUser(user) { + if (!user) return null + if (!user.active) return null + + // Process active user +} +``` + +### 6.9 Hoist RegExp Creation + +**Impact: LOW-MEDIUM** + +Create RegExp outside loops. + +```typescript +// Incorrect: creates regex on each iteration +items.forEach(item => { + if (/pattern/.test(item)) { /* ... */ } +}) + +// Correct: reuses regex +const pattern = /pattern/ +items.forEach(item => { + if (pattern.test(item)) { /* ... */ } +}) +``` + +### 6.10 Use Loop for Min/Max Instead of Sort + +**Impact: MEDIUM for large arrays** + +Use a loop instead of sorting to find min/max. + +```typescript +// Incorrect: O(n log n) +const max = items.sort((a, b) => b - a)[0] + +// Correct: O(n) +const max = Math.max(...items) +// Or for very large arrays: +let max = items[0] +for (let i = 1; i < items.length; i++) { + if (items[i] > max) max = items[i] +} +``` + +### 6.11 Use Set/Map for O(1) Lookups + +**Impact: HIGH for repeated lookups** + +Use Set for membership checks, Map for key-value lookups. + +```typescript +// Incorrect: O(n) +const isValid = validIds.includes(id) + +// Correct: O(1) +const validIdSet = new Set(validIds) +const isValid = validIdSet.has(id) +``` + +### 6.12 Use toSorted() for Immutability + +**Impact: LOW** + +Use toSorted() instead of sort() to avoid mutation. + +```typescript +// Mutates original +const sorted = items.sort((a, b) => a - b) + +// Returns new array +const sorted = items.toSorted((a, b) => a - b) +``` + +--- + +## 7. Advanced Patterns + +**Impact: LOW** + +### 7.1 Store Event Handlers in Refs + +**Impact: LOW (avoids effect re-runs)** + +Store handlers in refs to avoid effect dependencies. + +```tsx +function useEventListener(event, handler) { + const handlerRef = useRef(handler) + handlerRef.current = handler + + useEffect(() => { + const listener = (e) => handlerRef.current(e) + window.addEventListener(event, listener) + return () => window.removeEventListener(event, listener) + }, [event]) // handler not in deps +} +``` + +### 7.2 useLatest for Stable Callback Refs + +**Impact: LOW** + +Create a useLatest hook for stable references. + +```tsx +function useLatest(value) { + const ref = useRef(value) + ref.current = value + return ref +} + +function useInterval(callback, delay) { + const callbackRef = useLatest(callback) + + useEffect(() => { + const id = setInterval(() => callbackRef.current(), delay) + return () => clearInterval(id) + }, [delay]) +} +``` + +--- + +## References + +1. [https://github.com/shuding/better-all](https://github.com/shuding/better-all) +2. [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js) diff --git a/.claude/skills/vercel-react-best-practices/SKILL.md b/.claude/skills/vercel-react-best-practices/SKILL.md new file mode 100644 index 0000000..0b4f0b0 --- /dev/null +++ b/.claude/skills/vercel-react-best-practices/SKILL.md @@ -0,0 +1,111 @@ +--- +name: react-best-practices +description: React performance optimization guidelines. Use when writing, reviewing, or refactoring React code to ensure optimal performance patterns. Triggers on tasks involving React components, data fetching, bundle optimization, or performance improvements. +license: MIT +metadata: + author: vercel + version: "1.0.0" +--- + +# React Best Practices + +Performance optimization guide for React applications. Contains rules across 6 categories, prioritized by impact. + +## When to Apply + +Reference these guidelines when: +- Writing new React components +- Implementing data fetching +- Reviewing code for performance issues +- Refactoring existing React code +- Optimizing bundle size or load times + +## Rule Categories by Priority + +| Priority | Category | Impact | Prefix | +|----------|----------|--------|--------| +| 1 | Eliminating Waterfalls | CRITICAL | `async-` | +| 2 | Bundle Size Optimization | CRITICAL | `bundle-` | +| 3 | Client-Side Data Fetching | MEDIUM-HIGH | `client-` | +| 4 | Re-render Optimization | MEDIUM | `rerender-` | +| 5 | Rendering Performance | MEDIUM | `rendering-` | +| 6 | JavaScript Performance | LOW-MEDIUM | `js-` | +| 7 | Advanced Patterns | LOW | `advanced-` | + +## Quick Reference + +### 1. Eliminating Waterfalls (CRITICAL) + +- `async-defer-await` - Move await into branches where actually used +- `async-parallel` - Use Promise.all() for independent operations +- `async-dependencies` - Use better-all for partial dependencies + +### 2. Bundle Size Optimization (CRITICAL) + +- `bundle-barrel-imports` - Import directly, avoid barrel files +- `bundle-conditional` - Load modules only when feature is activated +- `bundle-preload` - Preload on hover/focus for perceived speed + +### 3. Client-Side Data Fetching (MEDIUM-HIGH) + +- `client-swr-dedup` - Use SWR for automatic request deduplication +- `client-event-listeners` - Deduplicate global event listeners +- `client-localstorage-schema` - Schema validation for localStorage +- `client-passive-event-listeners` - Use passive listeners for scroll/touch + +### 4. Re-render Optimization (MEDIUM) + +- `rerender-defer-reads` - Don't subscribe to state only used in callbacks +- `rerender-memo` - Extract expensive work into memoized components +- `rerender-dependencies` - Use primitive dependencies in effects +- `rerender-derived-state` - Subscribe to derived booleans, not raw values +- `rerender-functional-setstate` - Use functional setState for stable callbacks +- `rerender-lazy-state-init` - Pass function to useState for expensive values +- `rerender-transitions` - Use startTransition for non-urgent updates + +### 5. Rendering Performance (MEDIUM) + +- `rendering-animate-svg-wrapper` - Animate div wrapper, not SVG element +- `rendering-content-visibility` - Use content-visibility for long lists +- `rendering-hoist-jsx` - Extract static JSX outside components +- `rendering-svg-precision` - Reduce SVG coordinate precision +- `rendering-conditional-render` - Use ternary, not && for conditionals + +### 6. JavaScript Performance (LOW-MEDIUM) + +- `js-batch-dom-css` - Group CSS changes via classes or cssText +- `js-index-maps` - Build Map for repeated lookups +- `js-cache-property-access` - Cache object properties in loops +- `js-cache-function-results` - Cache function results in module-level Map +- `js-cache-storage` - Cache localStorage/sessionStorage reads +- `js-combine-iterations` - Combine multiple filter/map into one loop +- `js-length-check-first` - Check array length before expensive comparison +- `js-early-exit` - Return early from functions +- `js-hoist-regexp` - Hoist RegExp creation outside loops +- `js-min-max-loop` - Use loop for min/max instead of sort +- `js-set-map-lookups` - Use Set/Map for O(1) lookups +- `js-tosorted-immutable` - Use toSorted() for immutability + +### 7. Advanced Patterns (LOW) + +- `advanced-event-handler-refs` - Store event handlers in refs +- `advanced-use-latest` - useLatest for stable callback refs + +## How to Use + +Read individual rule files for detailed explanations and code examples: + +``` +rules/async-parallel.md +rules/bundle-barrel-imports.md +``` + +Each rule file contains: +- Brief explanation of why it matters +- Incorrect code example with explanation +- Correct code example with explanation +- Additional context and references + +## Full Compiled Document + +For the complete guide with all rules expanded: `AGENTS.md` diff --git a/.claude/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md b/.claude/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md new file mode 100644 index 0000000..3a45152 --- /dev/null +++ b/.claude/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md @@ -0,0 +1,55 @@ +--- +title: Store Event Handlers in Refs +impact: LOW +impactDescription: stable subscriptions +tags: advanced, hooks, refs, event-handlers, optimization +--- + +## Store Event Handlers in Refs + +Store callbacks in refs when used in effects that shouldn't re-subscribe on callback changes. + +**Incorrect (re-subscribes on every render):** + +```tsx +function useWindowEvent(event: string, handler: () => void) { + useEffect(() => { + window.addEventListener(event, handler) + return () => window.removeEventListener(event, handler) + }, [event, handler]) +} +``` + +**Correct (stable subscription):** + +```tsx +function useWindowEvent(event: string, handler: () => void) { + const handlerRef = useRef(handler) + useEffect(() => { + handlerRef.current = handler + }, [handler]) + + useEffect(() => { + const listener = () => handlerRef.current() + window.addEventListener(event, listener) + return () => window.removeEventListener(event, listener) + }, [event]) +} +``` + +**Alternative: use `useEffectEvent` if you're on latest React:** + +```tsx +import { useEffectEvent } from 'react' + +function useWindowEvent(event: string, handler: () => void) { + const onEvent = useEffectEvent(handler) + + useEffect(() => { + window.addEventListener(event, onEvent) + return () => window.removeEventListener(event, onEvent) + }, [event]) +} +``` + +`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler. diff --git a/.claude/skills/vercel-react-best-practices/rules/advanced-use-latest.md b/.claude/skills/vercel-react-best-practices/rules/advanced-use-latest.md new file mode 100644 index 0000000..3facdf2 --- /dev/null +++ b/.claude/skills/vercel-react-best-practices/rules/advanced-use-latest.md @@ -0,0 +1,49 @@ +--- +title: useLatest for Stable Callback Refs +impact: LOW +impactDescription: prevents effect re-runs +tags: advanced, hooks, useLatest, refs, optimization +--- + +## useLatest for Stable Callback Refs + +Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures. + +**Implementation:** + +```typescript +function useLatest(value: T) { + const ref = useRef(value) + useEffect(() => { + ref.current = value + }, [value]) + return ref +} +``` + +**Incorrect (effect re-runs on every callback change):** + +```tsx +function SearchInput({ onSearch }: { onSearch: (q: string) => void }) { + const [query, setQuery] = useState('') + + useEffect(() => { + const timeout = setTimeout(() => onSearch(query), 300) + return () => clearTimeout(timeout) + }, [query, onSearch]) +} +``` + +**Correct (stable effect, fresh callback):** + +```tsx +function SearchInput({ onSearch }: { onSearch: (q: string) => void }) { + const [query, setQuery] = useState('') + const onSearchRef = useLatest(onSearch) + + useEffect(() => { + const timeout = setTimeout(() => onSearchRef.current(query), 300) + return () => clearTimeout(timeout) + }, [query]) +} +``` diff --git a/.claude/skills/vercel-react-best-practices/rules/async-defer-await.md b/.claude/skills/vercel-react-best-practices/rules/async-defer-await.md new file mode 100644 index 0000000..ea7082a --- /dev/null +++ b/.claude/skills/vercel-react-best-practices/rules/async-defer-await.md @@ -0,0 +1,80 @@ +--- +title: Defer Await Until Needed +impact: HIGH +impactDescription: avoids blocking unused code paths +tags: async, await, conditional, optimization +--- + +## Defer Await Until Needed + +Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them. + +**Incorrect (blocks both branches):** + +```typescript +async function handleRequest(userId: string, skipProcessing: boolean) { + const userData = await fetchUserData(userId) + + if (skipProcessing) { + // Returns immediately but still waited for userData + return { skipped: true } + } + + // Only this branch uses userData + return processUserData(userData) +} +``` + +**Correct (only blocks when needed):** + +```typescript +async function handleRequest(userId: string, skipProcessing: boolean) { + if (skipProcessing) { + // Returns immediately without waiting + return { skipped: true } + } + + // Fetch only when needed + const userData = await fetchUserData(userId) + return processUserData(userData) +} +``` + +**Another example (early return optimization):** + +```typescript +// Incorrect: always fetches permissions +async function updateResource(resourceId: string, userId: string) { + const permissions = await fetchPermissions(userId) + const resource = await getResource(resourceId) + + if (!resource) { + return { error: 'Not found' } + } + + if (!permissions.canEdit) { + return { error: 'Forbidden' } + } + + return await updateResourceData(resource, permissions) +} + +// Correct: fetches only when needed +async function updateResource(resourceId: string, userId: string) { + const resource = await getResource(resourceId) + + if (!resource) { + return { error: 'Not found' } + } + + const permissions = await fetchPermissions(userId) + + if (!permissions.canEdit) { + return { error: 'Forbidden' } + } + + return await updateResourceData(resource, permissions) +} +``` + +This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive. diff --git a/.claude/skills/vercel-react-best-practices/rules/async-dependencies.md b/.claude/skills/vercel-react-best-practices/rules/async-dependencies.md new file mode 100644 index 0000000..fb90d86 --- /dev/null +++ b/.claude/skills/vercel-react-best-practices/rules/async-dependencies.md @@ -0,0 +1,36 @@ +--- +title: Dependency-Based Parallelization +impact: CRITICAL +impactDescription: 2-10× improvement +tags: async, parallelization, dependencies, better-all +--- + +## Dependency-Based Parallelization + +For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment. + +**Incorrect (profile waits for config unnecessarily):** + +```typescript +const [user, config] = await Promise.all([ + fetchUser(), + fetchConfig() +]) +const profile = await fetchProfile(user.id) +``` + +**Correct (config and profile run in parallel):** + +```typescript +import { all } from 'better-all' + +const { user, config, profile } = await all({ + async user() { return fetchUser() }, + async config() { return fetchConfig() }, + async profile() { + return fetchProfile((await this.$.user).id) + } +}) +``` + +Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all) diff --git a/.claude/skills/vercel-react-best-practices/rules/async-parallel.md b/.claude/skills/vercel-react-best-practices/rules/async-parallel.md new file mode 100644 index 0000000..64133f6 --- /dev/null +++ b/.claude/skills/vercel-react-best-practices/rules/async-parallel.md @@ -0,0 +1,28 @@ +--- +title: Promise.all() for Independent Operations +impact: CRITICAL +impactDescription: 2-10× improvement +tags: async, parallelization, promises, waterfalls +--- + +## Promise.all() for Independent Operations + +When async operations have no interdependencies, execute them concurrently using `Promise.all()`. + +**Incorrect (sequential execution, 3 round trips):** + +```typescript +const user = await fetchUser() +const posts = await fetchPosts() +const comments = await fetchComments() +``` + +**Correct (parallel execution, 1 round trip):** + +```typescript +const [user, posts, comments] = await Promise.all([ + fetchUser(), + fetchPosts(), + fetchComments() +]) +``` diff --git a/.claude/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md b/.claude/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md new file mode 100644 index 0000000..ee48f32 --- /dev/null +++ b/.claude/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md @@ -0,0 +1,59 @@ +--- +title: Avoid Barrel File Imports +impact: CRITICAL +impactDescription: 200-800ms import cost, slow builds +tags: bundle, imports, tree-shaking, barrel-files, performance +--- + +## Avoid Barrel File Imports + +Import directly from source files instead of barrel files to avoid loading thousands of unused modules. **Barrel files** are entry points that re-export multiple modules (e.g., `index.js` that does `export * from './module'`). + +Popular icon and component libraries can have **up to 10,000 re-exports** in their entry file. For many React packages, **it takes 200-800ms just to import them**, affecting both development speed and production cold starts. + +**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. + +**Incorrect (imports entire library):** + +```tsx +import { Check, X, Menu } from 'lucide-react' +// Loads 1,583 modules, takes ~2.8s extra in dev +// Runtime cost: 200-800ms on every cold start + +import { Button, TextField } from '@mui/material' +// Loads 2,225 modules, takes ~4.2s extra in dev +``` + +**Correct (imports only what you need):** + +```tsx +import Check from 'lucide-react/dist/esm/icons/check' +import X from 'lucide-react/dist/esm/icons/x' +import Menu from 'lucide-react/dist/esm/icons/menu' +// Loads only 3 modules (~2KB vs ~1MB) + +import Button from '@mui/material/Button' +import TextField from '@mui/material/TextField' +// Loads only what you use +``` + +**Alternative (Next.js 13.5+):** + +```js +// next.config.js - use optimizePackageImports +module.exports = { + experimental: { + optimizePackageImports: ['lucide-react', '@mui/material'] + } +} + +// Then you can keep the ergonomic barrel imports: +import { Check, X, Menu } from 'lucide-react' +// Automatically transformed to direct imports at build time +``` + +Direct imports provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR. + +Libraries commonly affected: `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `@headlessui/react`, `@radix-ui/react-*`, `lodash`, `ramda`, `date-fns`, `rxjs`, `react-use`. + +Reference: [How we optimized package imports in Next.js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js) diff --git a/.claude/skills/vercel-react-best-practices/rules/bundle-conditional.md b/.claude/skills/vercel-react-best-practices/rules/bundle-conditional.md new file mode 100644 index 0000000..99d6fc9 --- /dev/null +++ b/.claude/skills/vercel-react-best-practices/rules/bundle-conditional.md @@ -0,0 +1,31 @@ +--- +title: Conditional Module Loading +impact: HIGH +impactDescription: loads large data only when needed +tags: bundle, conditional-loading, lazy-loading +--- + +## Conditional Module Loading + +Load large data or modules only when a feature is activated. + +**Example (lazy-load animation frames):** + +```tsx +function AnimationPlayer({ enabled, setEnabled }: { enabled: boolean; setEnabled: React.Dispatch> }) { + const [frames, setFrames] = useState(null) + + useEffect(() => { + if (enabled && !frames && typeof window !== 'undefined') { + import('./animation-frames.js') + .then(mod => setFrames(mod.frames)) + .catch(() => setEnabled(false)) + } + }, [enabled, frames, setEnabled]) + + if (!frames) return + return +} +``` + +The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed. diff --git a/.claude/skills/vercel-react-best-practices/rules/bundle-preload.md b/.claude/skills/vercel-react-best-practices/rules/bundle-preload.md new file mode 100644 index 0000000..7000504 --- /dev/null +++ b/.claude/skills/vercel-react-best-practices/rules/bundle-preload.md @@ -0,0 +1,50 @@ +--- +title: Preload Based on User Intent +impact: MEDIUM +impactDescription: reduces perceived latency +tags: bundle, preload, user-intent, hover +--- + +## Preload Based on User Intent + +Preload heavy bundles before they're needed to reduce perceived latency. + +**Example (preload on hover/focus):** + +```tsx +function EditorButton({ onClick }: { onClick: () => void }) { + const preload = () => { + if (typeof window !== 'undefined') { + void import('./monaco-editor') + } + } + + return ( + + ) +} +``` + +**Example (preload when feature flag is enabled):** + +```tsx +function FlagsProvider({ children, flags }: Props) { + useEffect(() => { + if (flags.editorEnabled && typeof window !== 'undefined') { + void import('./monaco-editor').then(mod => mod.init()) + } + }, [flags.editorEnabled]) + + return + {children} + +} +``` + +The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed. diff --git a/.claude/skills/vercel-react-best-practices/rules/client-event-listeners.md b/.claude/skills/vercel-react-best-practices/rules/client-event-listeners.md new file mode 100644 index 0000000..aad4ae9 --- /dev/null +++ b/.claude/skills/vercel-react-best-practices/rules/client-event-listeners.md @@ -0,0 +1,74 @@ +--- +title: Deduplicate Global Event Listeners +impact: LOW +impactDescription: single listener for N components +tags: client, swr, event-listeners, subscription +--- + +## Deduplicate Global Event Listeners + +Use `useSWRSubscription()` to share global event listeners across component instances. + +**Incorrect (N instances = N listeners):** + +```tsx +function useKeyboardShortcut(key: string, callback: () => void) { + useEffect(() => { + const handler = (e: KeyboardEvent) => { + if (e.metaKey && e.key === key) { + callback() + } + } + window.addEventListener('keydown', handler) + return () => window.removeEventListener('keydown', handler) + }, [key, callback]) +} +``` + +When using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener. + +**Correct (N instances = 1 listener):** + +```tsx +import useSWRSubscription from 'swr/subscription' + +// Module-level Map to track callbacks per key +const keyCallbacks = new Map void>>() + +function useKeyboardShortcut(key: string, callback: () => void) { + // Register this callback in the Map + useEffect(() => { + if (!keyCallbacks.has(key)) { + keyCallbacks.set(key, new Set()) + } + keyCallbacks.get(key)!.add(callback) + + return () => { + const set = keyCallbacks.get(key) + if (set) { + set.delete(callback) + if (set.size === 0) { + keyCallbacks.delete(key) + } + } + } + }, [key, callback]) + + useSWRSubscription('global-keydown', () => { + const handler = (e: KeyboardEvent) => { + if (e.metaKey && keyCallbacks.has(e.key)) { + keyCallbacks.get(e.key)!.forEach(cb => cb()) + } + } + window.addEventListener('keydown', handler) + return () => window.removeEventListener('keydown', handler) + }) +} + +function Profile() { + // Multiple shortcuts will share the same listener + useKeyboardShortcut('p', () => { /* ... */ }) + useKeyboardShortcut('k', () => { /* ... */ }) + // ... +} +``` diff --git a/.claude/skills/vercel-react-best-practices/rules/client-localstorage-schema.md b/.claude/skills/vercel-react-best-practices/rules/client-localstorage-schema.md new file mode 100644 index 0000000..d30a1a7 --- /dev/null +++ b/.claude/skills/vercel-react-best-practices/rules/client-localstorage-schema.md @@ -0,0 +1,71 @@ +--- +title: Version and Minimize localStorage Data +impact: MEDIUM +impactDescription: prevents schema conflicts, reduces storage size +tags: client, localStorage, storage, versioning, data-minimization +--- + +## Version and Minimize localStorage Data + +Add version prefix to keys and store only needed fields. Prevents schema conflicts and accidental storage of sensitive data. + +**Incorrect:** + +```typescript +// No version, stores everything, no error handling +localStorage.setItem('userConfig', JSON.stringify(fullUserObject)) +const data = localStorage.getItem('userConfig') +``` + +**Correct:** + +```typescript +const VERSION = 'v2' + +function saveConfig(config: { theme: string; language: string }) { + try { + localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config)) + } catch { + // Throws in incognito/private browsing, quota exceeded, or disabled + } +} + +function loadConfig() { + try { + const data = localStorage.getItem(`userConfig:${VERSION}`) + return data ? JSON.parse(data) : null + } catch { + return null + } +} + +// Migration from v1 to v2 +function migrate() { + try { + const v1 = localStorage.getItem('userConfig:v1') + if (v1) { + const old = JSON.parse(v1) + saveConfig({ theme: old.darkMode ? 'dark' : 'light', language: old.lang }) + localStorage.removeItem('userConfig:v1') + } + } catch {} +} +``` + +**Store minimal fields from server responses:** + +```typescript +// User object has 20+ fields, only store what UI needs +function cachePrefs(user: FullUser) { + try { + localStorage.setItem('prefs:v1', JSON.stringify({ + theme: user.preferences.theme, + notifications: user.preferences.notifications + })) + } catch {} +} +``` + +**Always wrap in try-catch:** `getItem()` and `setItem()` throw in incognito/private browsing (Safari, Firefox), when quota exceeded, or when disabled. + +**Benefits:** Schema evolution via versioning, reduced storage size, prevents storing tokens/PII/internal flags. diff --git a/.claude/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md b/.claude/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md new file mode 100644 index 0000000..ce39a88 --- /dev/null +++ b/.claude/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md @@ -0,0 +1,48 @@ +--- +title: Use Passive Event Listeners for Scrolling Performance +impact: MEDIUM +impactDescription: eliminates scroll delay caused by event listeners +tags: client, event-listeners, scrolling, performance, touch, wheel +--- + +## Use Passive Event Listeners for Scrolling Performance + +Add `{ passive: true }` to touch and wheel event listeners to enable immediate scrolling. Browsers normally wait for listeners to finish to check if `preventDefault()` is called, causing scroll delay. + +**Incorrect:** + +```typescript +useEffect(() => { + const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX) + const handleWheel = (e: WheelEvent) => console.log(e.deltaY) + + document.addEventListener('touchstart', handleTouch) + document.addEventListener('wheel', handleWheel) + + return () => { + document.removeEventListener('touchstart', handleTouch) + document.removeEventListener('wheel', handleWheel) + } +}, []) +``` + +**Correct:** + +```typescript +useEffect(() => { + const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX) + const handleWheel = (e: WheelEvent) => console.log(e.deltaY) + + document.addEventListener('touchstart', handleTouch, { passive: true }) + document.addEventListener('wheel', handleWheel, { passive: true }) + + return () => { + document.removeEventListener('touchstart', handleTouch) + document.removeEventListener('wheel', handleWheel) + } +}, []) +``` + +**Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`. + +**Don't use passive when:** implementing custom swipe gestures, custom zoom controls, or any listener that needs `preventDefault()`. diff --git a/.claude/skills/vercel-react-best-practices/rules/client-swr-dedup.md b/.claude/skills/vercel-react-best-practices/rules/client-swr-dedup.md new file mode 100644 index 0000000..2a430f2 --- /dev/null +++ b/.claude/skills/vercel-react-best-practices/rules/client-swr-dedup.md @@ -0,0 +1,56 @@ +--- +title: Use SWR for Automatic Deduplication +impact: MEDIUM-HIGH +impactDescription: automatic deduplication +tags: client, swr, deduplication, data-fetching +--- + +## Use SWR for Automatic Deduplication + +SWR enables request deduplication, caching, and revalidation across component instances. + +**Incorrect (no deduplication, each instance fetches):** + +```tsx +function UserList() { + const [users, setUsers] = useState([]) + useEffect(() => { + fetch('/api/users') + .then(r => r.json()) + .then(setUsers) + }, []) +} +``` + +**Correct (multiple instances share one request):** + +```tsx +import useSWR from 'swr' + +function UserList() { + const { data: users } = useSWR('/api/users', fetcher) +} +``` + +**For immutable data:** + +```tsx +import { useImmutableSWR } from '@/lib/swr' + +function StaticContent() { + const { data } = useImmutableSWR('/api/config', fetcher) +} +``` + +**For mutations:** + +```tsx +import { useSWRMutation } from 'swr/mutation' + +function UpdateButton() { + const { trigger } = useSWRMutation('/api/user', updateUser) + return +} +``` + +Reference: [https://swr.vercel.app](https://swr.vercel.app) diff --git a/.claude/skills/vercel-react-best-practices/rules/js-batch-dom-css.md b/.claude/skills/vercel-react-best-practices/rules/js-batch-dom-css.md new file mode 100644 index 0000000..92a3b63 --- /dev/null +++ b/.claude/skills/vercel-react-best-practices/rules/js-batch-dom-css.md @@ -0,0 +1,82 @@ +--- +title: Batch DOM CSS Changes +impact: MEDIUM +impactDescription: reduces reflows/repaints +tags: javascript, dom, css, performance, reflow +--- + +## Batch DOM CSS Changes + +Avoid changing styles one property at a time. Group multiple CSS changes together via classes or `cssText` to minimize browser reflows. + +**Incorrect (multiple reflows):** + +```typescript +function updateElementStyles(element: HTMLElement) { + // Each line triggers a reflow + element.style.width = '100px' + element.style.height = '200px' + element.style.backgroundColor = 'blue' + element.style.border = '1px solid black' +} +``` + +**Correct (add class - single reflow):** + +```typescript +// CSS file +.highlighted-box { + width: 100px; + height: 200px; + background-color: blue; + border: 1px solid black; +} + +// JavaScript +function updateElementStyles(element: HTMLElement) { + element.classList.add('highlighted-box') +} +``` + +**Correct (change cssText - single reflow):** + +```typescript +function updateElementStyles(element: HTMLElement) { + element.style.cssText = ` + width: 100px; + height: 200px; + background-color: blue; + border: 1px solid black; + ` +} +``` + +**React example:** + +```tsx +// Incorrect: changing styles one by one +function Box({ isHighlighted }: { isHighlighted: boolean }) { + const ref = useRef(null) + + useEffect(() => { + if (ref.current && isHighlighted) { + ref.current.style.width = '100px' + ref.current.style.height = '200px' + ref.current.style.backgroundColor = 'blue' + } + }, [isHighlighted]) + + return
Content
+} + +// Correct: toggle class +function Box({ isHighlighted }: { isHighlighted: boolean }) { + return ( +
+ Content +
+ ) +} +``` + +Prefer CSS classes over inline styles when possible. Classes are cached by the browser and provide better separation of concerns. diff --git a/.claude/skills/vercel-react-best-practices/rules/js-cache-function-results.md b/.claude/skills/vercel-react-best-practices/rules/js-cache-function-results.md new file mode 100644 index 0000000..180f8ac --- /dev/null +++ b/.claude/skills/vercel-react-best-practices/rules/js-cache-function-results.md @@ -0,0 +1,80 @@ +--- +title: Cache Repeated Function Calls +impact: MEDIUM +impactDescription: avoid redundant computation +tags: javascript, cache, memoization, performance +--- + +## Cache Repeated Function Calls + +Use a module-level Map to cache function results when the same function is called repeatedly with the same inputs during render. + +**Incorrect (redundant computation):** + +```typescript +function ProjectList({ projects }: { projects: Project[] }) { + return ( +
+ {projects.map(project => { + // slugify() called 100+ times for same project names + const slug = slugify(project.name) + + return + })} +
+ ) +} +``` + +**Correct (cached results):** + +```typescript +// Module-level cache +const slugifyCache = new Map() + +function cachedSlugify(text: string): string { + if (slugifyCache.has(text)) { + return slugifyCache.get(text)! + } + const result = slugify(text) + slugifyCache.set(text, result) + return result +} + +function ProjectList({ projects }: { projects: Project[] }) { + return ( +
+ {projects.map(project => { + // Computed only once per unique project name + const slug = cachedSlugify(project.name) + + return + })} +
+ ) +} +``` + +**Simpler pattern for single-value functions:** + +```typescript +let isLoggedInCache: boolean | null = null + +function isLoggedIn(): boolean { + if (isLoggedInCache !== null) { + return isLoggedInCache + } + + isLoggedInCache = document.cookie.includes('auth=') + return isLoggedInCache +} + +// Clear cache when auth changes +function onAuthChange() { + isLoggedInCache = null +} +``` + +Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components. + +Reference: [How we made the Vercel Dashboard twice as fast](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast) diff --git a/.claude/skills/vercel-react-best-practices/rules/js-cache-property-access.md b/.claude/skills/vercel-react-best-practices/rules/js-cache-property-access.md new file mode 100644 index 0000000..39eec90 --- /dev/null +++ b/.claude/skills/vercel-react-best-practices/rules/js-cache-property-access.md @@ -0,0 +1,28 @@ +--- +title: Cache Property Access in Loops +impact: LOW-MEDIUM +impactDescription: reduces lookups +tags: javascript, loops, optimization, caching +--- + +## Cache Property Access in Loops + +Cache object property lookups in hot paths. + +**Incorrect (3 lookups × N iterations):** + +```typescript +for (let i = 0; i < arr.length; i++) { + process(obj.config.settings.value) +} +``` + +**Correct (1 lookup total):** + +```typescript +const value = obj.config.settings.value +const len = arr.length +for (let i = 0; i < len; i++) { + process(value) +} +``` diff --git a/.claude/skills/vercel-react-best-practices/rules/js-cache-storage.md b/.claude/skills/vercel-react-best-practices/rules/js-cache-storage.md new file mode 100644 index 0000000..aa4a30c --- /dev/null +++ b/.claude/skills/vercel-react-best-practices/rules/js-cache-storage.md @@ -0,0 +1,70 @@ +--- +title: Cache Storage API Calls +impact: LOW-MEDIUM +impactDescription: reduces expensive I/O +tags: javascript, localStorage, storage, caching, performance +--- + +## Cache Storage API Calls + +`localStorage`, `sessionStorage`, and `document.cookie` are synchronous and expensive. Cache reads in memory. + +**Incorrect (reads storage on every call):** + +```typescript +function getTheme() { + return localStorage.getItem('theme') ?? 'light' +} +// Called 10 times = 10 storage reads +``` + +**Correct (Map cache):** + +```typescript +const storageCache = new Map() + +function getLocalStorage(key: string) { + if (!storageCache.has(key)) { + storageCache.set(key, localStorage.getItem(key)) + } + return storageCache.get(key) +} + +function setLocalStorage(key: string, value: string) { + localStorage.setItem(key, value) + storageCache.set(key, value) // keep cache in sync +} +``` + +Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components. + +**Cookie caching:** + +```typescript +let cookieCache: Record | null = null + +function getCookie(name: string) { + if (!cookieCache) { + cookieCache = Object.fromEntries( + document.cookie.split('; ').map(c => c.split('=')) + ) + } + return cookieCache[name] +} +``` + +**Important (invalidate on external changes):** + +If storage can change externally (another tab, server-set cookies), invalidate cache: + +```typescript +window.addEventListener('storage', (e) => { + if (e.key) storageCache.delete(e.key) +}) + +document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'visible') { + storageCache.clear() + } +}) +``` diff --git a/.claude/skills/vercel-react-best-practices/rules/js-combine-iterations.md b/.claude/skills/vercel-react-best-practices/rules/js-combine-iterations.md new file mode 100644 index 0000000..044d017 --- /dev/null +++ b/.claude/skills/vercel-react-best-practices/rules/js-combine-iterations.md @@ -0,0 +1,32 @@ +--- +title: Combine Multiple Array Iterations +impact: LOW-MEDIUM +impactDescription: reduces iterations +tags: javascript, arrays, loops, performance +--- + +## Combine Multiple Array Iterations + +Multiple `.filter()` or `.map()` calls iterate the array multiple times. Combine into one loop. + +**Incorrect (3 iterations):** + +```typescript +const admins = users.filter(u => u.isAdmin) +const testers = users.filter(u => u.isTester) +const inactive = users.filter(u => !u.isActive) +``` + +**Correct (1 iteration):** + +```typescript +const admins: User[] = [] +const testers: User[] = [] +const inactive: User[] = [] + +for (const user of users) { + if (user.isAdmin) admins.push(user) + if (user.isTester) testers.push(user) + if (!user.isActive) inactive.push(user) +} +``` diff --git a/.claude/skills/vercel-react-best-practices/rules/js-early-exit.md b/.claude/skills/vercel-react-best-practices/rules/js-early-exit.md new file mode 100644 index 0000000..f46cb89 --- /dev/null +++ b/.claude/skills/vercel-react-best-practices/rules/js-early-exit.md @@ -0,0 +1,50 @@ +--- +title: Early Return from Functions +impact: LOW-MEDIUM +impactDescription: avoids unnecessary computation +tags: javascript, functions, optimization, early-return +--- + +## Early Return from Functions + +Return early when result is determined to skip unnecessary processing. + +**Incorrect (processes all items even after finding answer):** + +```typescript +function validateUsers(users: User[]) { + let hasError = false + let errorMessage = '' + + for (const user of users) { + if (!user.email) { + hasError = true + errorMessage = 'Email required' + } + if (!user.name) { + hasError = true + errorMessage = 'Name required' + } + // Continues checking all users even after error found + } + + return hasError ? { valid: false, error: errorMessage } : { valid: true } +} +``` + +**Correct (returns immediately on first error):** + +```typescript +function validateUsers(users: User[]) { + for (const user of users) { + if (!user.email) { + return { valid: false, error: 'Email required' } + } + if (!user.name) { + return { valid: false, error: 'Name required' } + } + } + + return { valid: true } +} +``` diff --git a/.claude/skills/vercel-react-best-practices/rules/js-hoist-regexp.md b/.claude/skills/vercel-react-best-practices/rules/js-hoist-regexp.md new file mode 100644 index 0000000..dae3fef --- /dev/null +++ b/.claude/skills/vercel-react-best-practices/rules/js-hoist-regexp.md @@ -0,0 +1,45 @@ +--- +title: Hoist RegExp Creation +impact: LOW-MEDIUM +impactDescription: avoids recreation +tags: javascript, regexp, optimization, memoization +--- + +## Hoist RegExp Creation + +Don't create RegExp inside render. Hoist to module scope or memoize with `useMemo()`. + +**Incorrect (new RegExp every render):** + +```tsx +function Highlighter({ text, query }: Props) { + const regex = new RegExp(`(${query})`, 'gi') + const parts = text.split(regex) + return <>{parts.map((part, i) => ...)} +} +``` + +**Correct (memoize or hoist):** + +```tsx +const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + +function Highlighter({ text, query }: Props) { + const regex = useMemo( + () => new RegExp(`(${escapeRegex(query)})`, 'gi'), + [query] + ) + const parts = text.split(regex) + return <>{parts.map((part, i) => ...)} +} +``` + +**Warning (global regex has mutable state):** + +Global regex (`/g`) has mutable `lastIndex` state: + +```typescript +const regex = /foo/g +regex.test('foo') // true, lastIndex = 3 +regex.test('foo') // false, lastIndex = 0 +``` diff --git a/.claude/skills/vercel-react-best-practices/rules/js-index-maps.md b/.claude/skills/vercel-react-best-practices/rules/js-index-maps.md new file mode 100644 index 0000000..9d357a0 --- /dev/null +++ b/.claude/skills/vercel-react-best-practices/rules/js-index-maps.md @@ -0,0 +1,37 @@ +--- +title: Build Index Maps for Repeated Lookups +impact: LOW-MEDIUM +impactDescription: 1M ops to 2K ops +tags: javascript, map, indexing, optimization, performance +--- + +## Build Index Maps for Repeated Lookups + +Multiple `.find()` calls by the same key should use a Map. + +**Incorrect (O(n) per lookup):** + +```typescript +function processOrders(orders: Order[], users: User[]) { + return orders.map(order => ({ + ...order, + user: users.find(u => u.id === order.userId) + })) +} +``` + +**Correct (O(1) per lookup):** + +```typescript +function processOrders(orders: Order[], users: User[]) { + const userById = new Map(users.map(u => [u.id, u])) + + return orders.map(order => ({ + ...order, + user: userById.get(order.userId) + })) +} +``` + +Build map once (O(n)), then all lookups are O(1). +For 1000 orders × 1000 users: 1M ops → 2K ops. diff --git a/.claude/skills/vercel-react-best-practices/rules/js-length-check-first.md b/.claude/skills/vercel-react-best-practices/rules/js-length-check-first.md new file mode 100644 index 0000000..6c38625 --- /dev/null +++ b/.claude/skills/vercel-react-best-practices/rules/js-length-check-first.md @@ -0,0 +1,49 @@ +--- +title: Early Length Check for Array Comparisons +impact: MEDIUM-HIGH +impactDescription: avoids expensive operations when lengths differ +tags: javascript, arrays, performance, optimization, comparison +--- + +## Early Length Check for Array Comparisons + +When comparing arrays with expensive operations (sorting, deep equality, serialization), check lengths first. If lengths differ, the arrays cannot be equal. + +In real-world applications, this optimization is especially valuable when the comparison runs in hot paths (event handlers, render loops). + +**Incorrect (always runs expensive comparison):** + +```typescript +function hasChanges(current: string[], original: string[]) { + // Always sorts and joins, even when lengths differ + return current.sort().join() !== original.sort().join() +} +``` + +Two O(n log n) sorts run even when `current.length` is 5 and `original.length` is 100. There is also overhead of joining the arrays and comparing the strings. + +**Correct (O(1) length check first):** + +```typescript +function hasChanges(current: string[], original: string[]) { + // Early return if lengths differ + if (current.length !== original.length) { + return true + } + // Only sort/join when lengths match + const currentSorted = current.toSorted() + const originalSorted = original.toSorted() + for (let i = 0; i < currentSorted.length; i++) { + if (currentSorted[i] !== originalSorted[i]) { + return true + } + } + return false +} +``` + +This new approach is more efficient because: +- It avoids the overhead of sorting and joining the arrays when lengths differ +- It avoids consuming memory for the joined strings (especially important for large arrays) +- It avoids mutating the original arrays +- It returns early when a difference is found diff --git a/.claude/skills/vercel-react-best-practices/rules/js-min-max-loop.md b/.claude/skills/vercel-react-best-practices/rules/js-min-max-loop.md new file mode 100644 index 0000000..02caec5 --- /dev/null +++ b/.claude/skills/vercel-react-best-practices/rules/js-min-max-loop.md @@ -0,0 +1,82 @@ +--- +title: Use Loop for Min/Max Instead of Sort +impact: LOW +impactDescription: O(n) instead of O(n log n) +tags: javascript, arrays, performance, sorting, algorithms +--- + +## Use Loop for Min/Max Instead of Sort + +Finding the smallest or largest element only requires a single pass through the array. Sorting is wasteful and slower. + +**Incorrect (O(n log n) - sort to find latest):** + +```typescript +interface Project { + id: string + name: string + updatedAt: number +} + +function getLatestProject(projects: Project[]) { + const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt) + return sorted[0] +} +``` + +Sorts the entire array just to find the maximum value. + +**Incorrect (O(n log n) - sort for oldest and newest):** + +```typescript +function getOldestAndNewest(projects: Project[]) { + const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt) + return { oldest: sorted[0], newest: sorted[sorted.length - 1] } +} +``` + +Still sorts unnecessarily when only min/max are needed. + +**Correct (O(n) - single loop):** + +```typescript +function getLatestProject(projects: Project[]) { + if (projects.length === 0) return null + + let latest = projects[0] + + for (let i = 1; i < projects.length; i++) { + if (projects[i].updatedAt > latest.updatedAt) { + latest = projects[i] + } + } + + return latest +} + +function getOldestAndNewest(projects: Project[]) { + if (projects.length === 0) return { oldest: null, newest: null } + + let oldest = projects[0] + let newest = projects[0] + + for (let i = 1; i < projects.length; i++) { + if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i] + if (projects[i].updatedAt > newest.updatedAt) newest = projects[i] + } + + return { oldest, newest } +} +``` + +Single pass through the array, no copying, no sorting. + +**Alternative (Math.min/Math.max for small arrays):** + +```typescript +const numbers = [5, 2, 8, 1, 9] +const min = Math.min(...numbers) +const max = Math.max(...numbers) +``` + +This works for small arrays but can be slower for very large arrays due to spread operator limitations. Use the loop approach for reliability. diff --git a/.claude/skills/vercel-react-best-practices/rules/js-set-map-lookups.md b/.claude/skills/vercel-react-best-practices/rules/js-set-map-lookups.md new file mode 100644 index 0000000..680a489 --- /dev/null +++ b/.claude/skills/vercel-react-best-practices/rules/js-set-map-lookups.md @@ -0,0 +1,24 @@ +--- +title: Use Set/Map for O(1) Lookups +impact: LOW-MEDIUM +impactDescription: O(n) to O(1) +tags: javascript, set, map, data-structures, performance +--- + +## Use Set/Map for O(1) Lookups + +Convert arrays to Set/Map for repeated membership checks. + +**Incorrect (O(n) per check):** + +```typescript +const allowedIds = ['a', 'b', 'c', ...] +items.filter(item => allowedIds.includes(item.id)) +``` + +**Correct (O(1) per check):** + +```typescript +const allowedIds = new Set(['a', 'b', 'c', ...]) +items.filter(item => allowedIds.has(item.id)) +``` diff --git a/.claude/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md b/.claude/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md new file mode 100644 index 0000000..eae8b3f --- /dev/null +++ b/.claude/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md @@ -0,0 +1,57 @@ +--- +title: Use toSorted() Instead of sort() for Immutability +impact: MEDIUM-HIGH +impactDescription: prevents mutation bugs in React state +tags: javascript, arrays, immutability, react, state, mutation +--- + +## Use toSorted() Instead of sort() for Immutability + +`.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. + +**Incorrect (mutates original array):** + +```typescript +function UserList({ users }: { users: User[] }) { + // Mutates the users prop array! + const sorted = useMemo( + () => users.sort((a, b) => a.name.localeCompare(b.name)), + [users] + ) + return
{sorted.map(renderUser)}
+} +``` + +**Correct (creates new array):** + +```typescript +function UserList({ users }: { users: User[] }) { + // Creates new sorted array, original unchanged + const sorted = useMemo( + () => users.toSorted((a, b) => a.name.localeCompare(b.name)), + [users] + ) + return
{sorted.map(renderUser)}
+} +``` + +**Why this matters in React:** + +1. Props/state mutations break React's immutability model - React expects props and state to be treated as read-only +2. Causes stale closure bugs - Mutating arrays inside closures (callbacks, effects) can lead to unexpected behavior + +**Browser support (fallback for older browsers):** + +`.toSorted()` is available in all modern browsers (Chrome 110+, Safari 16+, Firefox 115+, Node.js 20+). For older environments, use spread operator: + +```typescript +// Fallback for older browsers +const sorted = [...items].sort((a, b) => a.value - b.value) +``` + +**Other immutable array methods:** + +- `.toSorted()` - immutable sort +- `.toReversed()` - immutable reverse +- `.toSpliced()` - immutable splice +- `.with()` - immutable element replacement diff --git a/.claude/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md b/.claude/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md new file mode 100644 index 0000000..646744c --- /dev/null +++ b/.claude/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md @@ -0,0 +1,47 @@ +--- +title: Animate SVG Wrapper Instead of SVG Element +impact: LOW +impactDescription: enables hardware acceleration +tags: rendering, svg, css, animation, performance +--- + +## Animate SVG Wrapper Instead of SVG Element + +Many browsers don't have hardware acceleration for CSS3 animations on SVG elements. Wrap SVG in a `
` and animate the wrapper instead. + +**Incorrect (animating SVG directly - no hardware acceleration):** + +```tsx +function LoadingSpinner() { + return ( + + + + ) +} +``` + +**Correct (animating wrapper div - hardware accelerated):** + +```tsx +function LoadingSpinner() { + return ( +
+ + + +
+ ) +} +``` + +This applies to all CSS transforms and transitions (`transform`, `opacity`, `translate`, `scale`, `rotate`). The wrapper div allows browsers to use GPU acceleration for smoother animations. diff --git a/.claude/skills/vercel-react-best-practices/rules/rendering-conditional-render.md b/.claude/skills/vercel-react-best-practices/rules/rendering-conditional-render.md new file mode 100644 index 0000000..7e866f5 --- /dev/null +++ b/.claude/skills/vercel-react-best-practices/rules/rendering-conditional-render.md @@ -0,0 +1,40 @@ +--- +title: Use Explicit Conditional Rendering +impact: LOW +impactDescription: prevents rendering 0 or NaN +tags: rendering, conditional, jsx, falsy-values +--- + +## Use Explicit Conditional Rendering + +Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render. + +**Incorrect (renders "0" when count is 0):** + +```tsx +function Badge({ count }: { count: number }) { + return ( +
+ {count && {count}} +
+ ) +} + +// When count = 0, renders:
0
+// When count = 5, renders:
5
+``` + +**Correct (renders nothing when count is 0):** + +```tsx +function Badge({ count }: { count: number }) { + return ( +
+ {count > 0 ? {count} : null} +
+ ) +} + +// When count = 0, renders:
+// When count = 5, renders:
5
+``` diff --git a/.claude/skills/vercel-react-best-practices/rules/rendering-content-visibility.md b/.claude/skills/vercel-react-best-practices/rules/rendering-content-visibility.md new file mode 100644 index 0000000..aa66563 --- /dev/null +++ b/.claude/skills/vercel-react-best-practices/rules/rendering-content-visibility.md @@ -0,0 +1,38 @@ +--- +title: CSS content-visibility for Long Lists +impact: HIGH +impactDescription: faster initial render +tags: rendering, css, content-visibility, long-lists +--- + +## CSS content-visibility for Long Lists + +Apply `content-visibility: auto` to defer off-screen rendering. + +**CSS:** + +```css +.message-item { + content-visibility: auto; + contain-intrinsic-size: 0 80px; +} +``` + +**Example:** + +```tsx +function MessageList({ messages }: { messages: Message[] }) { + return ( +
+ {messages.map(msg => ( +
+ +
{msg.content}
+
+ ))} +
+ ) +} +``` + +For 1000 messages, browser skips layout/paint for ~990 off-screen items (10× faster initial render). diff --git a/.claude/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md b/.claude/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md new file mode 100644 index 0000000..32d2f3f --- /dev/null +++ b/.claude/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md @@ -0,0 +1,46 @@ +--- +title: Hoist Static JSX Elements +impact: LOW +impactDescription: avoids re-creation +tags: rendering, jsx, static, optimization +--- + +## Hoist Static JSX Elements + +Extract static JSX outside components to avoid re-creation. + +**Incorrect (recreates element every render):** + +```tsx +function LoadingSkeleton() { + return
+} + +function Container() { + return ( +
+ {loading && } +
+ ) +} +``` + +**Correct (reuses same element):** + +```tsx +const loadingSkeleton = ( +
+) + +function Container() { + return ( +
+ {loading && loadingSkeleton} +
+ ) +} +``` + +This is especially helpful for large and static SVG nodes, which can be expensive to recreate on every render. + +**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. diff --git a/.claude/skills/vercel-react-best-practices/rules/rendering-svg-precision.md b/.claude/skills/vercel-react-best-practices/rules/rendering-svg-precision.md new file mode 100644 index 0000000..6d77128 --- /dev/null +++ b/.claude/skills/vercel-react-best-practices/rules/rendering-svg-precision.md @@ -0,0 +1,28 @@ +--- +title: Optimize SVG Precision +impact: LOW +impactDescription: reduces file size +tags: rendering, svg, optimization, svgo +--- + +## Optimize SVG Precision + +Reduce SVG coordinate precision to decrease file size. The optimal precision depends on the viewBox size, but in general reducing precision should be considered. + +**Incorrect (excessive precision):** + +```svg + +``` + +**Correct (1 decimal place):** + +```svg + +``` + +**Automate with SVGO:** + +```bash +npx svgo --precision=1 --multipass icon.svg +``` diff --git a/.claude/skills/vercel-react-best-practices/rules/rerender-defer-reads.md b/.claude/skills/vercel-react-best-practices/rules/rerender-defer-reads.md new file mode 100644 index 0000000..e867c95 --- /dev/null +++ b/.claude/skills/vercel-react-best-practices/rules/rerender-defer-reads.md @@ -0,0 +1,39 @@ +--- +title: Defer State Reads to Usage Point +impact: MEDIUM +impactDescription: avoids unnecessary subscriptions +tags: rerender, searchParams, localStorage, optimization +--- + +## Defer State Reads to Usage Point + +Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks. + +**Incorrect (subscribes to all searchParams changes):** + +```tsx +function ShareButton({ chatId }: { chatId: string }) { + const searchParams = useSearchParams() + + const handleShare = () => { + const ref = searchParams.get('ref') + shareChat(chatId, { ref }) + } + + return +} +``` + +**Correct (reads on demand, no subscription):** + +```tsx +function ShareButton({ chatId }: { chatId: string }) { + const handleShare = () => { + const params = new URLSearchParams(window.location.search) + const ref = params.get('ref') + shareChat(chatId, { ref }) + } + + return +} +``` diff --git a/.claude/skills/vercel-react-best-practices/rules/rerender-dependencies.md b/.claude/skills/vercel-react-best-practices/rules/rerender-dependencies.md new file mode 100644 index 0000000..47a4d92 --- /dev/null +++ b/.claude/skills/vercel-react-best-practices/rules/rerender-dependencies.md @@ -0,0 +1,45 @@ +--- +title: Narrow Effect Dependencies +impact: LOW +impactDescription: minimizes effect re-runs +tags: rerender, useEffect, dependencies, optimization +--- + +## Narrow Effect Dependencies + +Specify primitive dependencies instead of objects to minimize effect re-runs. + +**Incorrect (re-runs on any user field change):** + +```tsx +useEffect(() => { + console.log(user.id) +}, [user]) +``` + +**Correct (re-runs only when id changes):** + +```tsx +useEffect(() => { + console.log(user.id) +}, [user.id]) +``` + +**For derived state, compute outside effect:** + +```tsx +// Incorrect: runs on width=767, 766, 765... +useEffect(() => { + if (width < 768) { + enableMobileMode() + } +}, [width]) + +// Correct: runs only on boolean transition +const isMobile = width < 768 +useEffect(() => { + if (isMobile) { + enableMobileMode() + } +}, [isMobile]) +``` diff --git a/.claude/skills/vercel-react-best-practices/rules/rerender-derived-state.md b/.claude/skills/vercel-react-best-practices/rules/rerender-derived-state.md new file mode 100644 index 0000000..e5c899f --- /dev/null +++ b/.claude/skills/vercel-react-best-practices/rules/rerender-derived-state.md @@ -0,0 +1,29 @@ +--- +title: Subscribe to Derived State +impact: MEDIUM +impactDescription: reduces re-render frequency +tags: rerender, derived-state, media-query, optimization +--- + +## Subscribe to Derived State + +Subscribe to derived boolean state instead of continuous values to reduce re-render frequency. + +**Incorrect (re-renders on every pixel change):** + +```tsx +function Sidebar() { + const width = useWindowWidth() // updates continuously + const isMobile = width < 768 + return