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 shared data hooks (e.g.
useApiQuery/useApiMutationwrappers over the query library) when it has them - 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