diff --git a/.agents/skills/review-react/SKILL.md b/.agents/skills/review-react/SKILL.md new file mode 100644 index 000000000..33b6885db --- /dev/null +++ b/.agents/skills/review-react/SKILL.md @@ -0,0 +1,80 @@ +--- +name: review-react +description: > + React code review guidelines covering Rules of React, re-render optimization, + rendering performance, and advanced patterns. Activates when writing, reviewing, + or refactoring React components, hooks, or state management code. +--- + +# React Code Review Guidelines + +Performance optimization and correctness guide for React applications. Contains 23 rules across 4 categories, prioritized by impact. + +## When to Apply + +Reference these guidelines when: +- Writing or reviewing React components and hooks +- Optimizing re-render performance +- Refactoring state management or effect logic +- Reviewing pull requests that touch React code + +## Rule Categories by Priority + +| Priority | Category | Impact | Prefix | Rules | +|----------|----------|--------|--------|-------| +| 1 | Rules of React | CRITICAL | `react-rules-` | 3 | +| 2 | Re-render Optimization | MEDIUM | `rerender-` | 13 | +| 3 | Rendering Performance | MEDIUM | `rendering-` | 5 | +| 4 | Advanced Patterns | LOW | `advanced-` | 2 | + +## Quick Reference + +### 1. Rules of React (CRITICAL) + +- `react-rules-purity` - Components and Hooks must be pure; no side effects during render +- `react-rules-hooks` - Only call Hooks at the top level and from React functions +- `react-rules-calling` - Never call components as functions or pass Hooks as values + +### 2. Re-render Optimization (MEDIUM) + +- `rerender-no-inline-components` - Never define components inside other components +- `rerender-derived-state-no-effect` - Derive state during render, not in effects +- `rerender-memo` - Extract memoized child components to avoid re-renders +- `rerender-memo-with-default-value` - Hoist default non-primitive props outside memo +- `rerender-simple-expression-in-memo` - Don't useMemo for simple primitive expressions +- `rerender-defer-reads` - Don't subscribe to state only used in callbacks +- `rerender-dependencies` - Use primitive values in effect dependencies +- `rerender-derived-state` - Subscribe to derived booleans, not raw objects +- `rerender-functional-setstate` - Use functional setState for stable callbacks +- `rerender-lazy-state-init` - Pass initializer function to useState for expensive values +- `rerender-move-effect-to-event` - Move interaction logic from effects to event handlers +- `rerender-transitions` - Use startTransition for non-urgent state updates +- `rerender-use-ref-transient-values` - Use refs for frequently-changing transient values + +### 3. Rendering Performance (MEDIUM) + +- `rendering-hoist-jsx` - Hoist static JSX outside component functions +- `rendering-conditional-render` - Use ternary operator instead of && for conditional rendering +- `rendering-usetransition-loading` - Prefer useTransition over manual loading state +- `rendering-content-visibility` - Use CSS content-visibility: auto for long lists +- `rendering-activity` - Use Activity component for preserving hidden UI state + +### 4. Advanced Patterns (LOW) + +- `advanced-event-handler-refs` - Store latest event handlers in refs for stable callbacks +- `advanced-init-once` - Initialize app-level singletons once, not per mount + +## How to Use + +Read individual rule files for detailed explanations and code examples: + +``` +rules/react-rules-purity.md +rules/rerender-no-inline-components.md +``` + +Each rule file contains: +- Brief explanation of why it matters +- Incorrect code example +- Correct code example +- Reference links diff --git a/.agents/skills/review-react/rules/_sections.md b/.agents/skills/review-react/rules/_sections.md new file mode 100644 index 000000000..f709e9be7 --- /dev/null +++ b/.agents/skills/review-react/rules/_sections.md @@ -0,0 +1,26 @@ +# Sections + +This file defines all sections, their ordering, impact levels, and descriptions. +The section ID (in parentheses) is the filename prefix used to group rules. + +--- + +## 1. Rules of React (react-rules) + +**Impact:** CRITICAL +**Description:** Fundamental rules from react.dev that ensure correctness. Components must be pure, Hooks must follow call rules, and components must not be called as functions. + +## 2. Re-render Optimization (rerender) + +**Impact:** MEDIUM +**Description:** Patterns to minimize unnecessary re-renders: proper memoization, derived state, functional setState, and effect dependency management. + +## 3. Rendering Performance (rendering) + +**Impact:** MEDIUM +**Description:** Techniques to optimize what and how React renders: hoisting static JSX, conditional rendering patterns, content-visibility, and transitions. + +## 4. Advanced Patterns (advanced) + +**Impact:** LOW +**Description:** Specialized techniques for edge cases: storing handlers in refs for stable callbacks and one-time initialization patterns. diff --git a/.agents/skills/review-react/rules/_template.md b/.agents/skills/review-react/rules/_template.md new file mode 100644 index 000000000..1e9e70703 --- /dev/null +++ b/.agents/skills/review-react/rules/_template.md @@ -0,0 +1,28 @@ +--- +title: Rule Title Here +impact: MEDIUM +impactDescription: Optional description of impact (e.g., "20-50% improvement") +tags: tag1, tag2 +--- + +## Rule Title Here + +**Impact: MEDIUM (optional impact description)** + +Brief explanation of the rule and why it matters. This should be clear and concise, explaining the performance implications. + +**Incorrect (description of what's wrong):** + +```typescript +// Bad code example here +const bad = example() +``` + +**Correct (description of what's right):** + +```typescript +// Good code example here +const good = example() +``` + +Reference: [Link to documentation or resource](https://example.com) diff --git a/.agents/skills/review-react/rules/advanced-event-handler-refs.md b/.agents/skills/review-react/rules/advanced-event-handler-refs.md new file mode 100644 index 000000000..97e7ade24 --- /dev/null +++ b/.agents/skills/review-react/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: (e) => void) { + useEffect(() => { + window.addEventListener(event, handler) + return () => window.removeEventListener(event, handler) + }, [event, handler]) +} +``` + +**Correct (stable subscription):** + +```tsx +function useWindowEvent(event: string, handler: (e) => void) { + const handlerRef = useRef(handler) + useEffect(() => { + handlerRef.current = handler + }, [handler]) + + useEffect(() => { + const listener = (e) => handlerRef.current(e) + 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: (e) => 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/.agents/skills/review-react/rules/advanced-init-once.md b/.agents/skills/review-react/rules/advanced-init-once.md new file mode 100644 index 000000000..73ee38e5e --- /dev/null +++ b/.agents/skills/review-react/rules/advanced-init-once.md @@ -0,0 +1,42 @@ +--- +title: Initialize App Once, Not Per Mount +impact: LOW-MEDIUM +impactDescription: avoids duplicate init in development +tags: initialization, useEffect, app-startup, side-effects +--- + +## Initialize App Once, Not Per Mount + +Do not put app-wide initialization that must run once per app load inside `useEffect([])` of a component. Components can remount and effects will re-run. Use a module-level guard or top-level init in the entry module instead. + +**Incorrect (runs twice in dev, re-runs on remount):** + +```tsx +function Comp() { + useEffect(() => { + loadFromStorage() + checkAuthToken() + }, []) + + // ... +} +``` + +**Correct (once per app load):** + +```tsx +let didInit = false + +function Comp() { + useEffect(() => { + if (didInit) return + didInit = true + loadFromStorage() + checkAuthToken() + }, []) + + // ... +} +``` + +Reference: [Initializing the application](https://react.dev/learn/you-might-not-need-an-effect#initializing-the-application) diff --git a/.agents/skills/review-react/rules/react-rules-calling.md b/.agents/skills/review-react/rules/react-rules-calling.md new file mode 100644 index 000000000..444318d6e --- /dev/null +++ b/.agents/skills/review-react/rules/react-rules-calling.md @@ -0,0 +1,66 @@ +--- +title: React Calls Components and Hooks +impact: CRITICAL +impactDescription: breaks React's rendering model and optimization +tags: react-rules, components, hooks, calling-convention +--- + +## React Calls Components and Hooks + +**Impact: CRITICAL (breaks React's rendering model and optimization)** + +React must control when components render and hooks execute. Calling them directly bypasses reconciliation, state management, and optimization. + +### Rule 1: Never call component functions directly + +**Incorrect (calling component as function):** + +```tsx +function Parent() { + // This bypasses React's rendering, no proper lifecycle or state isolation + return
{Profile({ name: 'Alice' })}
+} +``` + +**Correct (use JSX):** + +```tsx +function Parent() { + return
+} +``` + +Calling a component as a function makes React treat it as part of the parent's render. This means: +- No separate fiber node for reconciliation +- State and effects are tied to the parent +- Keys and refs don't work as expected + +### Rule 2: Never pass Hooks as regular values + +**Incorrect (passing hook as prop):** + +```tsx +function ChatRoom({ useStatus }) { + const status = useStatus() // Hook passed as value + return

{status}

+} + + +``` + +**Correct (call hook directly, pass result as prop):** + +```tsx +function ChatRoom({ status }) { + return

{status}

+} + +function ChatRoomWrapper() { + const status = useOnlineStatus() + return +} +``` + +Passing hooks as values makes them opaque to React's static analysis, breaks the Rules of Hooks, and prevents the compiler from optimizing correctly. + +Reference: [React calls Components and Hooks](https://react.dev/reference/rules/react-calls-components-and-hooks) diff --git a/.agents/skills/review-react/rules/react-rules-hooks.md b/.agents/skills/review-react/rules/react-rules-hooks.md new file mode 100644 index 000000000..6d98c5c7a --- /dev/null +++ b/.agents/skills/review-react/rules/react-rules-hooks.md @@ -0,0 +1,91 @@ +--- +title: Rules of Hooks +impact: CRITICAL +impactDescription: violating causes runtime errors and broken state +tags: react-rules, hooks, top-level +--- + +## Rules of Hooks + +**Impact: CRITICAL (violating causes runtime errors and broken state)** + +Hooks rely on a stable call order. Calling them conditionally or in loops breaks React's ability to track state correctly. + +### Rule 1: Only call Hooks at the top level + +**Incorrect (hook inside condition):** + +```tsx +function Form({ showName }) { + if (showName) { + const [name, setName] = useState('') // Conditional hook call + } + const [email, setEmail] = useState('') + return setEmail(e.target.value)} /> +} +``` + +**Correct (always call hooks, conditionally use values):** + +```tsx +function Form({ showName }) { + const [name, setName] = useState('') + const [email, setEmail] = useState('') + return ( + <> + {showName && setName(e.target.value)} />} + setEmail(e.target.value)} /> + + ) +} +``` + +**Incorrect (hook inside loop):** + +```tsx +function Filters({ filters }) { + const values = [] + for (const f of filters) { + values.push(useState(f.default)) // Hook in loop + } + return <>{/* ... */} +} +``` + +**Correct (extract to child component or use single state):** + +```tsx +function Filters({ filters }) { + const [values, setValues] = useState(() => + Object.fromEntries(filters.map(f => [f.id, f.default])) + ) + return <>{/* ... */} +} +``` + +### Rule 2: Only call Hooks from React functions + +**Incorrect (hook in regular function):** + +```tsx +function getUser() { + const [user, setUser] = useState(null) // Not a React component + return user +} +``` + +**Correct (hook in component or custom hook):** + +```tsx +function useUser() { + const [user, setUser] = useState(null) + return user +} + +function Profile() { + const user = useUser() + return

{user?.name}

+} +``` + +Reference: [Rules of Hooks](https://react.dev/reference/rules/rules-of-hooks) diff --git a/.agents/skills/review-react/rules/react-rules-purity.md b/.agents/skills/review-react/rules/react-rules-purity.md new file mode 100644 index 000000000..9f16fd5b9 --- /dev/null +++ b/.agents/skills/review-react/rules/react-rules-purity.md @@ -0,0 +1,121 @@ +--- +title: Components and Hooks Must Be Pure +impact: CRITICAL +impactDescription: prevents bugs from non-deterministic rendering +tags: react-rules, purity, side-effects, immutability +--- + +## Components and Hooks Must Be Pure + +**Impact: CRITICAL (prevents bugs from non-deterministic rendering)** + +React assumes components are pure functions: same inputs, same output. Side effects during render cause bugs that are hard to reproduce, especially with concurrent features and Strict Mode. + +### Rule 1: Components must be idempotent + +**Incorrect (mutating external variable during render):** + +```tsx +let count = 0 + +function Counter() { + count++ // Side effect during render + return

{count}

+} +``` + +**Correct (use state for mutable values):** + +```tsx +function Counter() { + const [count, setCount] = useState(0) + return

{count}

+} +``` + +### Rule 2: Side effects must run outside of render + +**Incorrect (side effect in render):** + +```tsx +function ProductList({ items }) { + analytics.track('viewed', items.length) // Runs on every render + return +} +``` + +**Correct (side effect in effect or event handler):** + +```tsx +function ProductList({ items }) { + useEffect(() => { + analytics.track('viewed', items.length) + }, [items.length]) + return +} +``` + +### Rule 3: Props and state are immutable + +**Incorrect (mutating props):** + +```tsx +function List({ items }) { + items.push({ id: 'last' }) // Mutating prop + return +} +``` + +**Correct (create new values):** + +```tsx +function List({ items }) { + const allItems = [...items, { id: 'last', name: 'Last' }] + return +} +``` + +### Rule 4: Return values and arguments to Hooks are immutable + +**Incorrect (mutating hook return value):** + +```tsx +function useItems() { + const items = useContext(ItemsContext) + items.sort() // Mutating context value + return items +} +``` + +**Correct (copy before mutating):** + +```tsx +function useItems() { + const items = useContext(ItemsContext) + return [...items].sort() +} +``` + +### Rule 5: Values are immutable after being passed to JSX + +**Incorrect (mutating after JSX use):** + +```tsx +function Page({ title }) { + const config = { title } + const element =
+ config.title = 'changed' // Mutating after JSX use + return element +} +``` + +**Correct (don't mutate after passing to JSX):** + +```tsx +function Page({ title }) { + const config = { title } + return
+} +``` + +Reference: [Components and Hooks must be pure](https://react.dev/reference/rules/components-and-hooks-must-be-pure) diff --git a/.agents/skills/review-react/rules/rendering-activity.md b/.agents/skills/review-react/rules/rendering-activity.md new file mode 100644 index 000000000..c957a490b --- /dev/null +++ b/.agents/skills/review-react/rules/rendering-activity.md @@ -0,0 +1,26 @@ +--- +title: Use Activity Component for Show/Hide +impact: MEDIUM +impactDescription: preserves state/DOM +tags: rendering, activity, visibility, state-preservation +--- + +## Use Activity Component for Show/Hide + +Use React's `` to preserve state/DOM for expensive components that frequently toggle visibility. + +**Usage:** + +```tsx +import { Activity } from 'react' + +function Dropdown({ isOpen }: Props) { + return ( + + + + ) +} +``` + +Avoids expensive re-renders and state loss. diff --git a/.agents/skills/review-react/rules/rendering-conditional-render.md b/.agents/skills/review-react/rules/rendering-conditional-render.md new file mode 100644 index 000000000..7e866f585 --- /dev/null +++ b/.agents/skills/review-react/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/.agents/skills/review-react/rules/rendering-content-visibility.md b/.agents/skills/review-react/rules/rendering-content-visibility.md new file mode 100644 index 000000000..aa6656362 --- /dev/null +++ b/.agents/skills/review-react/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/.agents/skills/review-react/rules/rendering-hoist-jsx.md b/.agents/skills/review-react/rules/rendering-hoist-jsx.md new file mode 100644 index 000000000..32d2f3fce --- /dev/null +++ b/.agents/skills/review-react/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/.agents/skills/review-react/rules/rendering-usetransition-loading.md b/.agents/skills/review-react/rules/rendering-usetransition-loading.md new file mode 100644 index 000000000..0c1b0b98e --- /dev/null +++ b/.agents/skills/review-react/rules/rendering-usetransition-loading.md @@ -0,0 +1,75 @@ +--- +title: Use useTransition Over Manual Loading States +impact: LOW +impactDescription: reduces re-renders and improves code clarity +tags: rendering, transitions, useTransition, loading, state +--- + +## Use useTransition Over Manual Loading States + +Use `useTransition` instead of manual `useState` for loading states. This provides built-in `isPending` state and automatically manages transitions. + +**Incorrect (manual loading state):** + +```tsx +function SearchResults() { + const [query, setQuery] = useState('') + const [results, setResults] = useState([]) + const [isLoading, setIsLoading] = useState(false) + + const handleSearch = async (value: string) => { + setIsLoading(true) + setQuery(value) + const data = await fetchResults(value) + setResults(data) + setIsLoading(false) + } + + return ( + <> + handleSearch(e.target.value)} /> + {isLoading && } + + + ) +} +``` + +**Correct (useTransition with built-in pending state):** + +```tsx +import { useTransition, useState } from 'react' + +function SearchResults() { + const [query, setQuery] = useState('') + const [results, setResults] = useState([]) + const [isPending, startTransition] = useTransition() + + const handleSearch = (value: string) => { + setQuery(value) // Update input immediately + + startTransition(async () => { + // Fetch and update results + const data = await fetchResults(value) + setResults(data) + }) + } + + return ( + <> + handleSearch(e.target.value)} /> + {isPending && } + + + ) +} +``` + +**Benefits:** + +- **Automatic pending state**: No need to manually manage `setIsLoading(true/false)` +- **Error resilience**: Pending state correctly resets even if the transition throws +- **Better responsiveness**: Keeps the UI responsive during updates +- **Interrupt handling**: New transitions automatically cancel pending ones + +Reference: [useTransition](https://react.dev/reference/react/useTransition) diff --git a/.agents/skills/review-react/rules/rerender-defer-reads.md b/.agents/skills/review-react/rules/rerender-defer-reads.md new file mode 100644 index 000000000..e867c95f0 --- /dev/null +++ b/.agents/skills/review-react/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/.agents/skills/review-react/rules/rerender-dependencies.md b/.agents/skills/review-react/rules/rerender-dependencies.md new file mode 100644 index 000000000..47a4d9268 --- /dev/null +++ b/.agents/skills/review-react/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/.agents/skills/review-react/rules/rerender-derived-state-no-effect.md b/.agents/skills/review-react/rules/rerender-derived-state-no-effect.md new file mode 100644 index 000000000..3d9fe4050 --- /dev/null +++ b/.agents/skills/review-react/rules/rerender-derived-state-no-effect.md @@ -0,0 +1,40 @@ +--- +title: Calculate Derived State During Rendering +impact: MEDIUM +impactDescription: avoids redundant renders and state drift +tags: rerender, derived-state, useEffect, state +--- + +## Calculate Derived State During Rendering + +If a value can be computed from current props/state, do not store it in state or update it in an effect. Derive it during render to avoid extra renders and state drift. Do not set state in effects solely in response to prop changes; prefer derived values or keyed resets instead. + +**Incorrect (redundant state and effect):** + +```tsx +function Form() { + const [firstName, setFirstName] = useState('First') + const [lastName, setLastName] = useState('Last') + const [fullName, setFullName] = useState('') + + useEffect(() => { + setFullName(firstName + ' ' + lastName) + }, [firstName, lastName]) + + return

{fullName}

+} +``` + +**Correct (derive during render):** + +```tsx +function Form() { + const [firstName, setFirstName] = useState('First') + const [lastName, setLastName] = useState('Last') + const fullName = firstName + ' ' + lastName + + return

{fullName}

+} +``` + +References: [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect) diff --git a/.agents/skills/review-react/rules/rerender-derived-state.md b/.agents/skills/review-react/rules/rerender-derived-state.md new file mode 100644 index 000000000..e5c899f6c --- /dev/null +++ b/.agents/skills/review-react/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