React & Next.js Coding Standards
Comprehensive coding standards for building production-grade React and Next.js applications. Follow them exactly. For detailed code examples, see REFERENCE.md.
When to Apply
Apply these standards to ALL React and Next.js work:
- Writing new components (server and client)
- Creating custom hooks
- State management setup and usage
- Writing tests (Vitest + React Testing Library)
- TypeScript types and interfaces for components
- Performance optimization
- Project structure decisions
- Form handling, data fetching, error boundaries
1. Component Rules
File Structure
- One component per file. File name matches component name in PascalCase.
"use client"only on the smallest interactive leaf components, never on large parent trees.- Default to Server Components. Only add
"use client"when you need state, effects, event handlers, or browser APIs.
Function Components
- Plain function declarations or arrow functions. No class components.
- No
React.forwardRef-- passrefdirectly as a prop (React 19+). - Props typed inline with
React.ComponentProps<"element">for extending native elements. - Props spread last:
{...props}always after default props so consumers can override.
Composition Over Configuration
- Accept
childrenas the primary composition mechanism. - Use compound components for complex UI (Modal, Dialog, Accordion).
- Share implicit state via Context between compound sub-components.
- Use
asChild/ Slot pattern for polymorphic rendering without extra DOM nodes.
Styling
- Tailwind-first with
cn()utility (clsx+tailwind-merge). - Use
cva(class-variance-authority) for variant systems. - Design tokens via CSS custom properties:
bg-primary,text-muted-foreground. - Dark mode via Tailwind
dark:variant. No separate dark theme files. data-slot="component-name"attribute on every component root for CSS targeting.classNamealways merged, never replaced:cn(defaultClasses, className).aria-invalid:variant for form validation styling.
Accessibility
- All interactive components need keyboard handling and ARIA attributes.
aria-labelledby,aria-describedby,aria-expanded,aria-haspopupon dialogs/menus.aria-pressed(string, not boolean) on toggle/filter buttons.- IDs generated via
useId()for uniqueness. - Focus trapping in modals. Return focus to trigger on close.
- Focus management on DOM swap: when edit/view modes toggle, use
useRef+useEffectto move focus. - Focus on deletion: when an item is deleted, focus the list heading or container.
tabIndex={-1}on headings/containers that need programmatic focus (not keyboard focusable).visually-hiddenclass for screen-reader-only text on icon buttons.role="list"on styled<ul>elements (CSS can strip list semantics).- Dev-only warnings for missing required a11y elements (title, description).
type="button"on non-submit buttons to prevent form submission.- Use
usePrevioushook to detect state transitions and avoid unwanted focus on initial render.
2. TypeScript Patterns
- Extend native element props:
React.ComponentProps<"button"> & CustomProps - Intersection types for variants:
React.ComponentProps<"button"> & VariantProps<typeof variants> - Generics with defaults for reusable hooks:
<T = unknown, E = Error> - Function overloads for different return types based on input config.
NoInfer<T>to prevent inference from return position.- Use
as constfor literal types. Use discriminated unions for tagged types. - Export types alongside components. Colocate types with their component.
- Zod for runtime validation schemas. Infer types with
z.infer<typeof schema>.
3. Custom Hook Rules
Design
- Single options object parameter, not positional args. More readable, extensible, TypeScript-friendly.
- Return stable references: wrap returned functions in
useCallback, objects inuseMemo. - Use
useSyncExternalStorefor subscribing to external state (localStorage, matchMedia, etc.). - Handle controlled/uncontrolled dual-mode with a
useControllableStatepattern.
Naming
useprefix always:useDebounce,useMediaQuery,useLocalStorage.- Name describes what it manages, not how it works.
- Return a tuple
[value, setter]for simple state, or a named object for complex state.
Common Patterns
useDebounce(value, delay) -- timer cleanup in useEffect
useLocalStorage(key, initial) -- useSyncExternalStore + StorageEvent
useMediaQuery(query) -- useSyncExternalStore + matchMedia
useControllableState(prop, default, onChange) -- controlled/uncontrolled
usePrevious(value) -- track previous state for transition detection
4. State Management
Decision Tree
- Local UI state --
useState. Keep state as local as possible. - Derived state -- Compute during render. No state needed.
- Shared across siblings -- Lift to nearest common parent.
- Shared across tree -- Context for low-frequency updates (theme, auth, locale).
- High-frequency global -- Zustand with selectors, or Jotai atoms.
- Server state -- TanStack Query (caching, revalidation, optimistic updates).
Context Rules
- Split context by domain. One context per concern (theme, auth, cart).
- Place providers as deep as possible in the tree.
- Context providers must be Client Components in Next.js.
<ThemeContext value="dark">(React 19, no.Providerneeded).
Zustand / Jotai
- Zustand: selector pattern prevents unnecessary re-renders.
useStore(store, selector). - Jotai: atomic model, each atom is independent.
useAtom(countAtom). - Both bridge external state into React via subscription + forced re-render.
5. Data Fetching
Server Components (preferred)
- Fetch data directly in Server Components with
async/await. - Use
cache()for request deduplication. - Parallel fetching: initiate all requests before awaiting any.
Server Actions
"use server"functions for mutations.- Use
useActionStatefor form submissions: returns[data, action, isPending]. useOptimisticfor instant UI feedback during async operations.useFormStatusfor pending state in submit buttons.
Client-Side
- TanStack Query for client-side server state.
- Single options object:
useQuery({ queryKey, queryFn }). - Never fetch in
useEffectfor data that should be server-fetched.
6. Performance
Architecture First (most impactful)
- Accept JSX as
children-- wrapper re-renders don't cascade. - Keep state local. Don't lift higher than necessary.
- Split context by domain. One update doesn't affect unrelated consumers.
- Server Components ship zero JS to the browser.
Memoization (only when measured)
React.memoonly after profiling confirms a benefit. Not a default.useMemofor calculations over 1ms (measure withconsole.time).useCallbackonly when passing functions tomemo-wrapped children.- Updater functions eliminate dependencies:
setTodos(prev => [...prev, item]). - React Compiler (v1.0) handles most memoization automatically in new projects.
Code Splitting
React.lazy()+Suspensefor route-based splitting.- Dynamic
import()for heavy components loaded on interaction. next/dynamicwithssr: falsefor browser-only libraries.
Lists and Rendering
- Virtualize lists over 100 items (
react-windoworreact-virtuoso). - Stable
keyprops. Never use array index as key for dynamic lists. useTransitionfor expensive updates that should not block input.useDeferredValueto show stale content while fresh content prepares.
Assets
next/imagefor automatic format negotiation, responsive sizing, lazy loading.next/fontfor automatic subsetting, no layout shift.- Prefer
transformandopacityfor animations (GPU compositor thread).
7. Error Handling
Error Boundaries
- Wrap route segments and independent features in Error Boundaries.
- Suspense handles loading. Error Boundary handles failure.
- Next.js: use
error.tsxfiles per route segment. - Provide a reset mechanism (
retrybutton) in fallback UI.
Patterns
- Class-based
ErrorBoundarywithgetDerivedStateFromError. - Or
react-error-boundarypackage for simpler API. - Dev-only runtime validation (guard with
process.env.NODE_ENV !== 'production').
8. Project Structure & Component Organization
Golden Rule: Break Everything Into Components
Every piece of UI that can be named gets its own component file. No monolithic pages. No 300-line files with inline JSX. Break aggressively, group logically.
COMPONENT DECISION: Can I name this piece of UI?
YES -> It is its own component file
NO -> It is too small (a single <span>) or too abstract
COMPONENT SIZE: If a component file exceeds 100 lines, it
probably contains multiple components that should be extracted.
Directory Structure
src/ (or resources/js/ for Inertia)
├── components/
│ ├── ui/ # Global design system primitives
│ │ ├── Button.tsx
│ │ ├── Dialog.tsx
│ │ ├── Input.tsx
│ │ ├── Badge.tsx
│ │ ├── Card.tsx
│ │ ├── DataTable.tsx
│ │ ├── Pagination.tsx
│ │ ├── EmptyState.tsx
│ │ ├── LoadingSpinner.tsx
│ │ └── index.ts # Barrel file (only here)
│ │
│ ├── layouts/ # Shared layout components
│ │ ├── AppLayout.tsx
│ │ ├── AuthLayout.tsx
│ │ ├── Sidebar.tsx
│ │ ├── Navbar.tsx
│ │ └── Footer.tsx
│ │
│ ├── orders/ # Page-specific components
│ │ ├── OrderCard.tsx
│ │ ├── OrderStatusBadge.tsx
│ │ ├── OrderFilters.tsx
│ │ ├── OrderTable.tsx
│ │ ├── OrderSummary.tsx
│ │ ├── OrderTimeline.tsx
│ │ └── CreateOrderForm.tsx
│ │
│ ├── dashboard/ # Dashboard-specific components
│ │ ├── StatsCard.tsx
│ │ ├── RevenueChart.tsx
│ │ ├── RecentOrders.tsx
│ │ └── ActivityFeed.tsx
│ │
│ ├── users/ # User-specific components
│ │ ├── UserAvatar.tsx
│ │ ├── UserCard.tsx
│ │ ├── UserTable.tsx
│ │ └── UserProfileForm.tsx
│ │
│ └── settings/ # Settings-specific components
│ ├── GeneralSettings.tsx
│ ├── NotificationSettings.tsx
│ └── BillingSettings.tsx
│
├── pages/ (or app/ for Next.js) # Page-level entry points (thin)
│ ├── Dashboard.tsx # Composes dashboard/* components
│ ├── Orders/
│ │ ├── Index.tsx # Composes orders/* components
│ │ ├── Show.tsx
│ │ └── Create.tsx
│ ├── Users/
│ │ ├── Index.tsx
│ │ └── Show.tsx
│ └── Settings/
│ └── Index.tsx
│
├── hooks/ # Custom hooks (one per file)
│ ├── useDebounce.ts
│ ├── useLocalStorage.ts
│ ├── useMediaQuery.ts
│ └── usePrevious.ts
│
├── lib/ # Utilities, API clients, config
│ ├── utils.ts # cn(), formatCurrency(), formatDate()
│ ├── api.ts
│ └── constants.ts
│
├── types/ # Shared TypeScript types
│ ├── order.ts
│ ├── user.ts
│ └── index.ts
│
└── actions/ # Server actions (Next.js)
├── orders.ts
└── users.ts
Component Hierarchy Rules
Global components (components/ui/):
- Used across 3+ pages or sections
- No business logic, pure presentation
- Accept generic props (children, className, variant, size)
- Examples: Button, Input, Dialog, Card, Badge, DataTable, Pagination
Layout components (components/layouts/):
- Page shells, navigation, sidebars, footers
- Shared across all or most pages
- Accept children for content injection
Page-specific components (components/{feature}/):
- Used by one section or feature
- Contain business logic specific to that feature
- Named with the feature prefix:
OrderCard,OrderTable,OrderFilters - Grouped in a folder matching the feature name
Page files (pages/ or app/):
- THIN. Pages are composers, not implementers.
- A page file imports and arranges components. That's it.
- No inline JSX beyond component composition.
- No business logic in page files.
// CORRECT: Page composes components
export default function OrdersIndex({ orders, filters }: Props) {
return (
<AppLayout>
<PageHeader title="Orders" />
<OrderFilters filters={filters} />
<OrderTable orders={orders} />
<Pagination meta={orders.meta} />
</AppLayout>
)
}
// WRONG: Page contains all the UI inline
export default function OrdersIndex({ orders }: Props) {
return (
<div className="...">
<h1>Orders</h1>
<div className="filters">
<select>...</select> // Should be <OrderFilters />
<input placeholder="Search"/> // Should be in <OrderFilters />
</div>
<table>
<thead>...</thead> // Should be <OrderTable />
<tbody>
{orders.map(order => ( // Should be inside <OrderTable />
<tr>...</tr>
))}
</tbody>
</table>
</div>
)
}
When to Extract a Component
Extract into its own file when:
1. It has a name (OrderCard, UserAvatar, StatsChart)
2. It is used more than once
3. It represents a logical chunk (filters, table, form, sidebar)
4. The parent file is getting long (100+ lines)
5. It has its own state or event handlers
6. It could be tested independently
Do NOT extract when:
1. It is a single HTML element with no logic
2. Extracting would create a file with less than 10 lines
3. It is only a wrapper div with className
Nesting Depth
Subdirectories when a feature is complex: components/orders/table/OrderTable.tsx, components/orders/form/CreateOrderForm.tsx.
Constants Outside Components
Static data outside the component function. Never recreate on every render. See REFERENCE.md for full example.
File Naming
- Components: PascalCase matching the component name (
OrderCard.tsx) - Hooks: camelCase matching the hook name (
useDebounce.ts) - Utils/lib: camelCase (
utils.ts,formatCurrency.ts) - Types: camelCase (
order.ts,user.ts) - Pages: PascalCase (
Index.tsx,Show.tsx,Create.tsx) - Barrel files: only in
components/ui/(index.ts) - One component per file. Always.
9. Testing (Vitest + React Testing Library)
Framework
- Vitest exclusively. Not Jest.
- React Testing Library for component tests.
- Test hooks by rendering real components that use them. Not
renderHook.
Naming
- Descriptive behavioral names:
it('should display error when form is invalid'). - Describe behavior, not implementation.
Patterns
// Render with providers
function renderWithProviders(ui: React.ReactElement) {
return render(
<QueryClientProvider client={queryClient}>
<ThemeProvider>{ui}</ThemeProvider>
</QueryClientProvider>
)
}
// Behavioral assertions
expect(screen.getByText('Submit')).toBeInTheDocument()
expect(screen.getByRole('button')).toBeDisabled()
// Async
await waitFor(() => expect(screen.getByText('Loaded')).toBeInTheDocument())
// User events
await userEvent.click(screen.getByRole('button', { name: 'Save' }))
// State transitions (collector pattern)
const states: Result[] = []
function Wrapper() {
const state = useQuery(options)
states.push(state)
return <div>{state.data}</div>
}
Rules
- Behavioral assertions over snapshots. Test what the user sees.
vi.useFakeTimers()for deterministic async tests.act()wrapping for synchronous state updates.- Track render counts to verify optimization.
- Wrap in
<StrictMode>to catch double-render bugs. - Mock with
vi.fn()andvi.spyOn(). UsemockRestore()in cleanup.
10. Form Gotchas
defaultChecked="false"sets a truthy STRING. UsedefaultChecked={false}with braces.checkedwithoutonChangecreates a read-only checkbox. UsedefaultCheckedfor uncontrolled.htmlForin JSX, notfor.classNamein JSX, notclass.- Always
event.preventDefault()in form submit handlers. - Controlled inputs:
value={state}+onChange={handler}. Both required together.
11. Event Handling
- Compose event handlers, never silently override:
composeEventHandlers(props.onClick, internalHandler). - Consumer handler runs first, can
preventDefault()to cancel internal handler. - Use updater form of setState in callbacks to avoid stale closures.
onPointerDownoveronClickfor drag and dismiss interactions.
Summary Checklist
Before submitting any React code:
- Server Component by default,
"use client"only on interactive leaves - No
forwardRef-- ref as a prop (React 19) - Props spread last, className merged via
cn() - TypeScript types extend native element props
- Custom hooks return stable references
- State as local as possible, context split by domain
- Data fetched in Server Components when possible
-
React.memo/useMemo/useCallbackonly after profiling - Error boundaries on route segments and features
- Tests use Vitest + RTL, behavioral assertions, real component rendering
- Accessibility: keyboard handling, ARIA attributes, focus management
- Stable keys on lists, virtualized if over 100 items