---
name: react-architecture
description: Architect-level guidance for structuring React applications — folder layout, component composition, state management selection, data fetching, routing, TypeScript conventions, and scaling a codebase to many teams. Use this whenever the user is starting a React project, refactoring a large React codebase, choosing between state libraries (Redux, Zustand, Jotai, TanStack Query, Context), deciding on Next.js vs Vite vs Remix, designing a component library, or asks "how should I structure this" for anything React. Also use it when reviewing React code for architectural smells, even if the user only asks about one component.
metadata:
  technology: React
  type: architecture
---

# React Architecture

Think like a staff engineer who will still own this codebase in three years. Every decision below trades short-term convenience for long-term clarity; explain the trade-off to the user rather than dictating.

## 1. Decide the runtime first

Before touching folders, settle these — they constrain everything else:

| Question | Default answer | Change it when |
|---|---|---|
| Framework | **Next.js (App Router)** for anything with SEO, auth, or server data. **Vite + React Router** for SPAs behind a login, dashboards, internal tools. | Team has no Node hosting → Vite. Heavy content/marketing → Astro with React islands. |
| Rendering | Server Components by default in Next.js; `"use client"` only at interaction boundaries. | SPA: everything is client; don't fake SSR. |
| Language | TypeScript, `strict: true`, no `any` escapes without a comment. | Never. |
| Styling | Tailwind + a headless component layer (Radix / React Aria) or an existing design system. | Design team ships tokens as CSS variables → CSS Modules consuming those tokens. |
| Package manager | pnpm (workspaces, strict hoisting). | Existing monorepo standard. |

Write these five decisions into an `ARCHITECTURE.md` at the repo root. Future contributors (human and AI) read that before anything else.

## 2. Folder structure: feature-first, not type-first

Type-first (`components/`, `hooks/`, `utils/`) collapses at ~50 components because nothing tells you what belongs together. Use **feature slices**:

```
src/
├── app/                 # routes only (Next.js) or router config (Vite). Thin.
├── features/
│   ├── checkout/
│   │   ├── components/  # used only by checkout
│   │   ├── hooks/
│   │   ├── api/         # queries, mutations, types from the server
│   │   ├── model/       # pure domain logic, no React
│   │   └── index.ts     # PUBLIC API — the only import path other features may use
│   └── catalog/
├── shared/
│   ├── ui/              # design-system primitives (Button, Dialog) — zero business logic
│   ├── lib/             # framework-agnostic helpers (date, money, fetch client)
│   └── hooks/           # generic hooks (useMediaQuery, useDebounce)
└── entities/            # optional: cross-feature domain types (User, Product)
```

Enforce boundaries with ESLint (`eslint-plugin-boundaries` or `no-restricted-imports`): features import from `shared/` and other features' `index.ts` only. When a feature needs something from another feature's internals, that is the signal to promote it to `shared/` or `entities/`, not to reach in.

## 3. Component composition rules

- **Compound components over prop explosion.** When a component crosses ~8 props or has boolean props that conflict (`isCompact`, `isFull`), split into `<Card><Card.Header/><Card.Body/></Card>`.
- **Container/presentational is dead; colocate.** A component owns its data fetching via a hook it calls. Separate only when the same UI is reused with different data sources.
- **Lift state to the lowest common ancestor, no higher.** Global state is a smell until two unrelated routes need the same value.
- **`children` and render props for slots**, not `renderHeader` + `renderFooter` + `renderX`.
- **One component per file, named export matching the file name.** Default exports break refactoring tools.
- **Server Component by default** (Next.js). Adding `"use client"` should feel like a small cost; if a client component is large, push its interactive part down into a leaf.

## 4. State management: choose by lifetime, not by popularity

Ask "how long does this value live, and who reads it?"

| State kind | Tool | Why |
|---|---|---|
| Server data (lists, records, anything with a URL) | **TanStack Query** (or SWR, or RSC + `fetch` cache in Next.js) | Caching, dedup, background refetch, optimistic updates are solved problems. Never put server data in Redux. |
| URL state (filters, pagination, selected tab) | **The URL** via router search params (`nuqs` in Next.js) | Shareable, back-button friendly, survives refresh. |
| Form state | **React Hook Form + Zod** (or TanStack Form) | Uncontrolled inputs, schema-driven validation. |
| Local UI state (open/closed, hover) | `useState` / `useReducer` | Nothing else. |
| Cross-component client state (theme, cart, auth session) | **Zustand** (simple) or **Jotai** (atomic, derived) | Small, no boilerplate, works outside React. |
| Complex, event-sourced, audited state | **Redux Toolkit** | Only when you truly need middleware, time-travel, or strict action logs. |
| Dependency injection (services, config) | React Context, changed rarely | Context is for injection, not for frequently-changing values — every consumer re-renders. |

If the user reaches for Redux "because it's standard", show them the table and ask which row their state is in.

## 5. Data layer conventions

- Generate types from the API contract: OpenAPI → `openapi-typescript`, GraphQL → codegen, tRPC for full-stack TS. Hand-written response types drift.
- One `apiClient` in `shared/lib` wrapping `fetch` with auth, base URL, error normalisation. Features never call `fetch` directly.
- Query keys as factories: `catalogKeys.list(filters)`, `catalogKeys.detail(id)`. Prevents cache-key typos.
- Mutations invalidate by key prefix, never by refetching "everything".
- Validate untrusted responses at the boundary with Zod when the backend is not TypeScript-owned.

## 6. TypeScript conventions that scale

- `type` for shapes, `interface` only when declaration merging is needed.
- Discriminated unions for variant props: `{ kind: 'link'; href: string } | { kind: 'button'; onClick: () => void }`.
- `satisfies` for config objects; `as const` for literal maps.
- Export prop types: `export type ButtonProps = ComponentPropsWithoutRef<'button'> & {...}`.
- No `React.FC` — use plain function declarations; `FC` obscures generics and `children` typing.
- Path aliases (`@/features/...`) via `tsconfig.paths`; never `../../../`.

## 7. Routing and code splitting

- Route-level `lazy()` (React Router) or automatic segment splitting (Next.js) — every route is its own chunk.
- Prefetch on hover/viewport for likely next routes.
- Loaders/`loader` functions or RSC fetch data *before* render; avoid waterfalls where a component mounts, then fetches, then a child mounts and fetches.
- Error boundaries per route segment with a designed fallback, plus one root boundary that reports to Sentry/Datadog.

## 8. Monorepo and multi-team scale

When more than one team ships to the same app:

- **Turborepo or Nx** with `apps/` and `packages/` (`packages/ui`, `packages/config-eslint`, `packages/tsconfig`).
- Each package has its own `package.json`, builds with `tsup`, and is consumed via workspace protocol.
- Versioned, documented design system in `packages/ui` with Storybook; product features never fork a primitive.
- Module federation only when independent deploys are a hard requirement — it adds real operational cost.

## 9. Non-negotiables checklist

Before calling an architecture "done", confirm:

- [ ] `ARCHITECTURE.md` exists with the five runtime decisions and the folder convention
- [ ] ESLint enforces import boundaries; CI fails on violation
- [ ] `tsc --noEmit` and lint run in CI; `strict` is on
- [ ] Server data lives in a query library, not in global client state
- [ ] Every route has an error boundary and a loading state
- [ ] No component file exceeds ~200 lines without a documented reason
- [ ] Storybook (or equivalent) exists for `shared/ui`
- [ ] Environment config validated at startup (Zod on `process.env`)

## Anti-patterns to call out immediately

- `useEffect` to sync derived state — compute it during render or with `useMemo`.
- Fetching in `useEffect` with manual `loading`/`error` flags — use a query library.
- Prop-drilling a `setState` four levels down — colocate or use a store.
- A `utils/` folder with 80 files — split by domain into features or `shared/lib/<topic>`.
- Barrel files (`index.ts`) re-exporting *everything* from `shared/` — they defeat tree-shaking; barrels are for feature public APIs only.
- Context used for fast-changing state (mouse position, form values).

## When the user asks "which is better, X or Y"

Give the decision table row, name the one you would pick for *their* stated constraints, and state the one condition that would flip the decision. Don't hedge with "it depends" alone.
