React Best Practices & Anti-Patterns
Modern React conventions (2025-26). Covers what to do and what to avoid. For code examples, see examples.md.
Severity Levels
Each rule is tagged with a severity for use by consuming agents:
- •CRITICAL — Will cause bugs, broken reconciliation, or maintenance nightmares
- •HIGH — Will cause performance issues or scaling problems
- •MEDIUM — Will hurt maintainability or developer experience
Component Design (CRITICAL)
- •Components must be pure — same inputs = same outputs, no side effects during render
- •Business logic in hooks/helpers, NOT in component bodies
- •Container components fetch data; presentational components receive props and render UI
- •Helper functions extracted OUTSIDE the component body
- •Max 200 lines per component — split if larger
- •Max 5-7 props — more suggests the component does too much
- •One component per file (small colocated internal helpers are fine)
Composition Patterns
- •Compose small focused components over monolithic ones
- •"Lift Content Up" — move children to the parent when the wrapper doesn't use them for logic
- •"Push State Down" — keep state in the component that actually needs it
- •Prefer
childrenprop and composition over deep prop drilling
Derive, Don't Store (CRITICAL)
The #1 React anti-pattern. Look for it in every review.
- •NEVER store derived values in
useState— compute during render - •NEVER use
useState+useEffectto sync a computed value — just compute it - •Use
useMemoONLY if the computation is expensive (measured, not assumed) - •If a value can be calculated from existing props/state, calculate it inline
State Management (HIGH)
State Location
- •Colocate state with the components that use it
- •Don't lift state higher than necessary — it causes unnecessary re-renders
- •Don't duplicate state across components
Context API
- •Context is for dependency injection (auth, theme), NOT global state management
- •Context changes re-render ALL consumers — split contexts by concern
- •Prefer hook return values over Context when only one subtree needs the data
State Hygiene
- •Not everything needs to be in
useState— only values that change over time and affect the UI - •Combine related state with
useReducerinstead of multipleuseStatecalls - •URL-dependent state (filters, pagination, search) belongs in URL search params, not component state
Hooks (HIGH)
useEffect Rules
Before each useEffect, ask: Is there an external system being synchronized? If no, it's misused.
- •NEVER use
useEffectfor derived state — compute during render - •NEVER use
useEffectfor event handling — put logic in the event handler - •NEVER chain
useEffects that trigger each other — usually means derived state - •ALWAYS declare all dependencies correctly
- •ALWAYS clean up subscriptions, timers, and event listeners
Memoization Rules
Most useMemo/useCallback calls are unnecessary. Be skeptical:
- •
useMemo— only for actually expensive computations (sorting large datasets, complex transforms) - •
useCallback— only when the function is passed to aReact.memochild - •Simple string concatenation, arithmetic, or boolean checks do NOT need memoization
Render Factories (CRITICAL)
camelCase functions returning JSX are NOT React components. They break reconciliation, hooks, and dev tools.
- •NEVER use
renderThing()pattern — use<Thing />component syntax - •ALWAYS use PascalCase for anything that returns JSX
- •Render factories lose component identity on every render, causing full unmount/remount
Inline Creation in JSX (HIGH)
New arrays, objects, and functions created inline in JSX props break React.memo on children.
- •Extract static arrays/objects to module-level constants
- •Extract dynamic arrays/objects to
useMemo - •Extract inline functions to
useCallback(only when passed to memoized children)
Over-Engineering (CRITICAL)
Premature Abstraction
- •Abstractions with only one consumer are premature — inline it
- •"Reusable" hooks with hardcoded field names are not reusable — accept config as parameters
- •Context storing state that could be computed locally is over-engineered
Wrapper Components
- •Components that only call a hook and return
nullare unnecessary — call the hook directly - •Wrappers that only pass props through add indirection without value
Data Fetching (HIGH)
- •ALL data fetching in custom hooks, never in component bodies
- •Use the project's
useApiQuery/useApiMutationcore hooks - •Handle loading, error, and empty states in the container component
- •Use try-catch in async functions within hooks
Tailwind CSS (MEDIUM)
- •Use utility classes for all styling — no inline
style={}objects - •Use responsive prefixes (
sm:,md:,lg:) for responsive design - •Extract repeated class combinations into reusable components (Button, Card, Badge)
- •Prefer the project's
components/ui/over recreating common elements
Error Boundaries (HIGH)
- •Use
react-error-boundarypackage for function component-friendly API - •Include
resetKeys={[location.pathname]}so boundaries reset on navigation - •Provide a "Try again" button that calls
resetErrorBoundaryin fallback UI - •Error boundaries do NOT catch errors in event handlers, async code, or SSR — use try/catch there
Key Prop Patterns (CRITICAL)
- •NEVER use array index as
keywhen lists can be reordered, filtered, or modified - •NEVER use
Math.random()or other unstable values as keys — causes full unmount/remount - •When mapping Fragments, put
keyon<React.Fragment key={id}>, not on a child
Conditional Rendering (HIGH)
- •NEVER use
{count && <Component />}whencountcan be0— renders literal0 - •Use
{count > 0 && <Component />}or ternary instead - •Replace nested ternaries with early returns or extracted components
- •For multiple UI states (loading/error/empty/success), use early returns pattern
Accessibility (HIGH)
- •Add
aria-labelto icon-only buttons — without it, they're invisible to screen readers - •Link error messages to fields with
aria-describedbyandaria-invalid - •Use
aria-live="polite"for dynamic content updates (search results, notifications, toasts) - •Trap focus inside modals; provide escape path (Escape key + visible Close button)
- •Announce route changes for screen readers (SPA navigation is silent by default)
Performance Beyond Memoization (MEDIUM)
- •Use
React.lazy()+<Suspense>for route-level code splitting - •Use Vite
manualChunksto split vendor bundles for better caching - •Use top-level static paths in
lazy(() => import('./X'))— dynamic paths break build analysis
Axios + React Patterns (HIGH)
- •Cancel in-flight requests in
useEffectcleanup usingAbortController - •Use centralized Axios instance with
baseURL, default headers, and interceptors - •Use request interceptors for auth tokens, response interceptors for 401/403 handling
React 19 Patterns (MEDIUM)
- •Accept
refas a regular prop instead of usingforwardRef(React 19+) - •With React Compiler enabled, avoid adding
memo/useMemo/useCallbackunless measured
Code Organization (MEDIUM)
Feature-Based Structure
- •Colocate component + hook + helpers + tests per feature
- •Shared utilities go in
utils/orcomponents/ui/
File Quality
- •Order: imports, constants, helpers, component, exports
- •Reuse existing types and constants over creating new ones