React Expert
UI is a pure function of state. Derive during render; use effects only to sync with the outside world. Most "React performance" problems are really "I created an effect/state I didn't need."
When to Use
- Building or refactoring React components and custom hooks.
- Bugs: stale state, infinite render loops, effects firing too often, lost input focus, extra re-renders.
- Choosing state management (local vs lifted vs context vs server cache vs store).
- Effects, refs, memoization, Suspense, or Server Components.
When NOT to Use
- Pure TypeScript typing →
typescript-expert.
- Next.js routing/server actions/caching specifics →
nextjs-expert.
- Styling only →
tailwind-expert. Accessibility semantics → accessibility-expert.
Core Principles
1. Render is pure; derive don't sync
- Compute values from props/state during render. Don't mirror props into state, then sync with
useEffect — that adds a render and bugs.
- Reset component state with a
key, not an effect. Adjust state during render (the rare "store previous value" pattern) instead of in an effect when possible.
2. useEffect is an escape hatch
- It's for synchronizing with external systems (network, subscriptions, DOM, timers) — not for transforming data or responding to events.
- Event logic belongs in event handlers, not effects. If an effect only runs because a user did something, it's probably wrong.
- Always clean up (abort fetches, clear timers, unsubscribe) in the returned function. Give complete dependency arrays; if a dep "shouldn't" re-run things, restructure (move it into a ref, a reducer, or out of the component).
3. State, placed correctly
- Local
useState → useReducer when transitions are related/complex → lift to the closest common parent → Context for low-frequency global values (theme, auth) → external store (Zustand/Redux) for high-frequency or cross-cutting state.
- Server data is not UI state — use TanStack Query or RSC fetching for caching, dedup, and revalidation. Don't hand-roll it in
useEffect.
4. Performance: measure, then memo
- Profile with React DevTools first. Memoize only proven hot paths or referential-stability needs.
useMemo/useCallback cost memory + comparison; sprinkling them everywhere is net-negative. React.memo only helps if props are referentially stable.
- React 19's compiler can auto-memoize — prefer clean code over manual memo when it's available.
5. Keys & lists
- Stable, identity-based
key (an ID), never the array index for reorderable/insertable lists — index keys corrupt state and inputs.
Decision Guide
| Need |
Reach for |
| Value computed from props/state |
derive in render (no state/effect) |
| Reset state when a prop changes |
key={...} |
| Server data (fetch/cache/refetch) |
TanStack Query / RSC |
| Global low-frequency value |
Context |
| High-frequency / cross-tree state |
Zustand/Redux |
| Imperative DOM / mutable value across renders |
useRef |
Common Mistakes
- Derived state mirrored into
useState + effect → derive instead.
- Missing/over-trimmed effect deps → stale closures or loops; fix the cause, don't lie to the linter.
useEffect for data fetching without cleanup → race conditions on fast prop changes; abort on cleanup or use a query lib.
- Index as key in dynamic lists → wrong item state after reorder/delete.
- New object/array/function in render passed to memoized child → defeats
React.memo.
- Calling hooks conditionally → breaks the rules of hooks.
Examples
Derive, don't store-and-sync
// ❌ extra state + effect (renders twice, can desync)
const [full, setFull] = useState("");
useEffect(() => setFull(`${first} ${last}`), [first, last]);
// ✅
const full = `${first} ${last}`;
Fetch effect that survives fast prop changes
useEffect(() => {
const ctrl = new AbortController();
let active = true;
fetch(`/api/user/${id}`, { signal: ctrl.signal })
.then((r) => r.json())
.then((u) => active && setUser(u))
.catch((e) => { if (e.name !== "AbortError") setError(e); });
return () => { active = false; ctrl.abort(); };
}, [id]);
See Also
nextjs-expert — App Router, RSC, and server-side data fetching.
typescript-expert — typing props, generic components, and events.
tailwind-expert — styling. accessibility-expert — accessible interactions.
performance-expert — profiling render hotspots.
1---2name: react-expert3description: Expert modern React (18/19): hooks, component design, state, performance, and Server Components. Trigger keywords: React, hooks, useState, useEffect, useMemo, useCallback, useRef, context, re-render, key, Suspense, Server Components, RSC, JSX, stale closure. Use when building/refactoring components or hooks, fixing render/effect bugs, or choosing a state-management approach.4---56# React Expert78> UI is a pure function of state. Derive during render; use effects only to sync with the outside world. Most "React performance" problems are really "I created an effect/state I didn't need."910## When to Use11- Building or refactoring React components and custom hooks.12- Bugs: stale state, infinite render loops, effects firing too often, lost input focus, extra re-renders.13- Choosing state management (local vs lifted vs context vs server cache vs store).14- Effects, refs, memoization, Suspense, or Server Components.1516## When NOT to Use17- Pure TypeScript typing → `typescript-expert`.18- Next.js routing/server actions/caching specifics → `nextjs-expert`.19- Styling only → `tailwind-expert`. Accessibility semantics → `accessibility-expert`.2021## Core Principles2223### 1. Render is pure; derive don't sync24- Compute values from props/state **during render**. Don't mirror props into state, then sync with `useEffect` — that adds a render and bugs.25- Reset component state with a `key`, not an effect. Adjust state during render (the rare "store previous value" pattern) instead of in an effect when possible.2627### 2. `useEffect` is an escape hatch28- It's for **synchronizing with external systems** (network, subscriptions, DOM, timers) — not for transforming data or responding to events.29- Event logic belongs in event handlers, not effects. If an effect only runs because a user did something, it's probably wrong.30- Always clean up (abort fetches, clear timers, unsubscribe) in the returned function. Give complete dependency arrays; if a dep "shouldn't" re-run things, restructure (move it into a ref, a reducer, or out of the component).3132### 3. State, placed correctly33- Local `useState` → `useReducer` when transitions are related/complex → **lift** to the closest common parent → Context for low-frequency global values (theme, auth) → external store (Zustand/Redux) for high-frequency or cross-cutting state.34- **Server data is not UI state** — use TanStack Query or RSC fetching for caching, dedup, and revalidation. Don't hand-roll it in `useEffect`.3536### 4. Performance: measure, then memo37- Profile with React DevTools first. Memoize only proven hot paths or referential-stability needs.38- `useMemo`/`useCallback` cost memory + comparison; sprinkling them everywhere is net-negative. `React.memo` only helps if props are referentially stable.39- React 19's compiler can auto-memoize — prefer clean code over manual memo when it's available.4041### 5. Keys & lists42- Stable, identity-based `key` (an ID), never the array index for reorderable/insertable lists — index keys corrupt state and inputs.4344## Decision Guide45| Need | Reach for |46|------|-----------|47| Value computed from props/state | derive in render (no state/effect) |48| Reset state when a prop changes | `key={...}` |49| Server data (fetch/cache/refetch) | TanStack Query / RSC |50| Global low-frequency value | Context |51| High-frequency / cross-tree state | Zustand/Redux |52| Imperative DOM / mutable value across renders | `useRef` |5354## Common Mistakes55- **Derived state mirrored into `useState` + effect** → derive instead.56- **Missing/over-trimmed effect deps** → stale closures or loops; fix the cause, don't lie to the linter.57- **`useEffect` for data fetching without cleanup** → race conditions on fast prop changes; abort on cleanup or use a query lib.58- **Index as key** in dynamic lists → wrong item state after reorder/delete.59- **New object/array/function in render passed to memoized child** → defeats `React.memo`.60- **Calling hooks conditionally** → breaks the rules of hooks.6162## Examples6364**Derive, don't store-and-sync**65```jsx66// ❌ extra state + effect (renders twice, can desync)67const [full, setFull] = useState("");68useEffect(() => setFull(`${first} ${last}`), [first, last]);69// ✅70const full = `${first} ${last}`;71```7273**Fetch effect that survives fast prop changes**74```jsx75useEffect(() => {76 const ctrl = new AbortController();77 let active = true;78 fetch(`/api/user/${id}`, { signal: ctrl.signal })79 .then((r) => r.json())80 .then((u) => active && setUser(u))81 .catch((e) => { if (e.name !== "AbortError") setError(e); });82 return () => { active = false; ctrl.abort(); };83}, [id]);84```8586## See Also87- `nextjs-expert` — App Router, RSC, and server-side data fetching.88- `typescript-expert` — typing props, generic components, and events.89- `tailwind-expert` — styling. `accessibility-expert` — accessible interactions.90- `performance-expert` — profiling render hotspots.