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.