Org Skills

react-architecture

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.

Download .zip Raw Source
When agents use itUse 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.

Install

Copilot (VS Code, Visual Studio, Copilot CLI and github.com) reads skills from the repository — commit them so the whole team gets them.

npx skills add AGCO-Global/org-skills --skill react-architecture -a github-copilot
# or with the org installer (adds .github/skills/react-architecture):
npx -y github:AGCO-Global/org-skills add skill react-architecture

Uses the open skills CLI. Works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, OpenCode, Windsurf and 60+ others — it asks which agent to install into.

npx skills add AGCO-Global/org-skills --skill react-architecture
# user-level instead of project-level:
npx skills add AGCO-Global/org-skills --skill react-architecture -g

Installs the frontend-react-skills plugin, which bundles all Frontend / React skills and keeps them updated.

/plugin marketplace add AGCO-Global/org-skills
/plugin install frontend-react-skills@org-skills
# or just this skill, in this repository:
npx skills add AGCO-Global/org-skills --skill react-architecture -a claude-code

Use Download .zip above, then upload it under Customize → Skills → Upload skill. Team and Enterprise admins can sync this repo as a plugin marketplace instead.

Installs into .agents/skills/, which Codex reads.

npx skills add AGCO-Global/org-skills --skill react-architecture -a codex

Installs into Cursor's skills folder.

npx skills add AGCO-Global/org-skills --skill react-architecture -a cursor

Installs into Gemini CLI's skills folder.

npx skills add AGCO-Global/org-skills --skill react-architecture -a gemini-cli

Any tool with rules, instructions or custom prompts: use Copy SKILL.md above and paste it in. It is plain Markdown.

Commands use your normal git sign-in to GitHub, so they work while the repository is private. Node.js 20+ required.

Skill contents

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.