Back to React
React
medium
mid

How do you structure a scalable React application?

Feature-folder structure (group by feature, not by file type), clear layers (UI primitives → domain components → pages), absolute imports, shared utilities/types in lib, server state via React Query, route-level code splitting, error boundaries per route, ESLint + Prettier + TypeScript + tests, monorepo if multiple apps share code. Keep boundaries explicit: features import from lib, not from each other. Document deviations in README.

9 min read·~5 min to think through

A scalable React app's structure is about minimizing the cost of change as the team and codebase grow.

Feature-folder layout

Group files by feature, not by file type. /components + /hooks + /utils at the root flatten everything; /features/checkout keeps related code together.

ts
src/
  app/                    # router-level entry, layouts, providers
    layout.tsx
    page.tsx
    error.tsx
  features/
    checkout/
      components/
        CheckoutForm.tsx
        PaymentSummary.tsx
      hooks/
        useCheckout.ts
      api.ts              # data-layer for this feature
      types.ts
      index.ts            # public exports
    products/

    settings/

  components/             # generic UI primitives (Button, Modal)
    Button.tsx
    Modal.tsx
  lib/                    # shared utilities
    api.ts                # fetch wrapper
    auth.ts
    format.ts
  hooks/                  # cross-feature hooks
    useDebounce.ts
  styles/

Layered architecture

  1. App shell (app/): providers, error boundaries, layouts.
  2. Pages / routes: orchestrate features for a URL.
  3. Features (features/): domain logic + components. Cohesive, self-contained.
  4. UI primitives (components/): generic, reusable, design-system-y. Used by features.
  5. Lib (lib/): cross-cutting utilities, API client, auth helpers.

Dependency rule: features depend on lib and primitives, not on each other. If checkout imports from products, that's a code smell — extract the shared bit to lib or a new shared feature.

State

  • Local: useState in the component.
  • Form: React Hook Form.
  • Server data: React Query / RTKQ / SWR.
  • Shared client state: Context (rarely changes) or Zustand/Jotai (frequently).
  • URL state: router params.

Don't put everything in one global store.

Routing

  • File-based (Next.js, Remix) or config-based (React Router).
  • Route-level code splitting: every route is its own chunk.
  • Per-route error boundary so one page error doesn't nuke the app.
  • Per-route loading state.

Data fetching

  • One thin fetch wrapper in lib/api.ts (timeout, auth, error normalization).
  • React Query for caching.
  • Per-feature api.ts defining endpoints + hooks.

Type safety

  • TypeScript everywhere.
  • Schema validation at boundaries (Zod for API responses, form input).
  • tsc --noEmit in CI.
  • Strict mode on.

Testing

  • Unit: pure functions, hooks (testing-library/react-hooks).
  • Integration: feature flows (testing-library/react with MSW for API mocks).
  • E2E: critical paths (Playwright).
  • Visual regression: Chromatic / Percy for design system.

Tooling

  • ESLint with react, react-hooks, jsx-a11y, import-order plugins.
  • Prettier for formatting.
  • Husky + lint-staged for pre-commit.
  • CI runs: lint, typecheck, test, build, size-limit, Lighthouse.
  • size-limit / Lighthouse CI for perf budgets.

Imports

  • Absolute imports via tsconfig paths (@/features/checkout not ../../../features/checkout).
  • Public API per feature via index.ts. Other features import only what's exported.
  • ESLint no-restricted-imports to enforce dependency rules.

Naming

  • PascalCase for components.
  • camelCase for functions/hooks.
  • kebab-case or PascalCase for file names — pick one and be consistent.
  • use* prefix for hooks.

Monorepo (when needed)

If you have multiple apps sharing code (web + admin + marketing):

  • pnpm / npm workspaces + Turborepo or Nx.
  • Shared design system as a package (@org/ui).
  • Shared API client / types (@org/api).
  • Per-app config, per-package builds.
  • Incremental builds with cache.

What to avoid

  • One mega /components folder with 200 files.
  • Cross-feature imports (checkout importing from products directly).
  • Putting server data in Redux.
  • One global state object for everything.
  • CSR for content pages.
  • No CI guards (lint, types, bundle size).
  • Missing error boundaries.

Documentation

  • README per feature explaining purpose + public API.
  • CHANGELOG for breaking changes.
  • ADRs (Architecture Decision Records) for significant choices (state library, router, styling).
  • Storybook for the design system.

Evolving the structure

  • Start simple (/components, /pages); refactor to features when growing.
  • Extract shared code only when needed by 2+ features (rule of three).
  • Refactor when the structure feels wrong, not on a schedule.

Mental model

Scalable structure isn't about predicting the future — it's about making each change cheap. Cohesion (related code together) and clear boundaries (explicit dependencies) are the two principles. Pick a structure early, enforce it in lint, evolve when pain shows up.

Follow-up questions

  • When do you switch from /components to feature folders?
  • How do you enforce dependency rules between features?
  • When is a monorepo worth it?
  • What goes in lib vs components?

Common mistakes

  • One mega /components folder.
  • Cross-feature imports — tangled dependencies.
  • No public API per feature — everything is internal.
  • No CI guards (lint, types, size).
  • Server data in client store.
  • Inconsistent file naming across teams.

Performance considerations

  • Architecture decisions (rendering mode, state placement, code splitting) set the perf ceiling. They're easier to get right early than to refactor later. Feature folders + route-level code splitting + budget-enforced bundle size keep perf hygiene as the codebase grows.

Edge cases

  • Migrating from Pages Router to App Router — gradual, route by route.
  • Adopting a design system mid-project — extract piece by piece.
  • Splitting a mono-app into microfrontends — Module Federation has tradeoffs.
  • Multi-tenant: theming + i18n hooked at the router layer.
  • Internationalization: per-locale code-split.

Real-world examples

  • Next.js App Router structure with features grouped.
  • Vercel, Linear, Stripe — production codebases with strong feature-folder patterns.
  • Bulletproof React (github.com/alan2207/bulletproof-react) — well-documented reference architecture.

Senior engineer discussion

Seniors define the structure early, codify it in lint/CI, and document deviations. They prioritize cohesion and explicit boundaries, treat the design system as a shared product, and evolve the structure when pain shows up rather than chasing trends.

Related questions