React Best Practices
Pair with TypeScript
When working with React, always load both this skill and typescript-best-practices together. TypeScript patterns (type-first development, discriminated unions, Zod validation) apply to React code.
Core Principle: Effects Are Escape Hatches
Effects let you "step outside" React to synchronize with external systems. Most component logic should NOT use Effects. Before writing an Effect, ask: "Is there a way to do this without an Effect?"
Decision Tree
- Need to respond to user interaction? Use event handler
- Need computed value from props/state? Calculate during render
- Need cached expensive calculation? Use
useMemo
- Need to reset state on prop change? Use
key prop
- Need to synchronize with external system? Use Effect with cleanup
- Need non-reactive code in Effect? Use
useEffectEvent
- Need mutable value that doesn't trigger render? Use ref
When to Use Effects
Synchronizing with external systems: browser APIs (WebSocket, IntersectionObserver), third-party non-React libraries, window/document event listeners, non-React DOM elements (video, maps).
When NOT to Use Effects
- Derived state — calculate during render
- Expensive calculations — use
useMemo
- Resetting state on prop change — use
key prop
- Responding to user events — use event handlers
- Notifying parent of state changes — update both in the same event handler
- Chains of effects — calculate derived state and update in one event handler
Refs
- Use for values that don't affect rendering (timer IDs, DOM node references)
- Never read or write
ref.current during render; only in event handlers and effects
- Use ref callbacks (not
useRef in loops) for dynamic lists
- Use
useImperativeHandle to limit what parent can access
Custom Hooks
- Share logic, not state — each call gets an independent state instance
- Name
useXxx only if it actually calls other hooks; otherwise use a regular function
- Avoid lifecycle hooks (
useMount, useEffectOnce) — use useEffect directly so the linter catches missing deps
- Keep focused on a single concrete use case
Component Patterns
- Controlled: parent owns state; uncontrolled: component owns state
- Prefer composition with
children over prop drilling
- Treat boolean props that switch large component trees (
isEditing, isThread, hideAttachments) as a composition smell; prefer separate composed components for distinct use cases
- For complex reusable UI, prefer compound components with provider-scoped state/actions over monolithic components with many optional props
- Use Context for scoped component families as well as truly global state, when it defines a local interface consumed by descendants
- Render JSX directly for UI variation; avoid config-array mini-frameworks unless the config is real domain data
- Lift the provider boundary when sibling or external controls need access to the same state/actions
- Use
flushSync when you need to read the DOM synchronously after a state update
See react-patterns.md for code examples and detailed patterns.