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.
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.
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
- App shell (
app/): providers, error boundaries, layouts. - Pages / routes: orchestrate features for a URL.
- Features (
features/): domain logic + components. Cohesive, self-contained. - UI primitives (
components/): generic, reusable, design-system-y. Used by features. - 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
fetchwrapper inlib/api.ts(timeout, auth, error normalization). - React Query for caching.
- Per-feature
api.tsdefining endpoints + hooks.
Type safety
- TypeScript everywhere.
- Schema validation at boundaries (Zod for API responses, form input).
tsc --noEmitin 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/checkoutnot../../../features/checkout). - Public API per feature via
index.ts. Other features import only what's exported. - ESLint
no-restricted-importsto 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
/componentsfolder with 200 files. - Cross-feature imports (
checkoutimporting fromproductsdirectly). - 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.