ReactArchitectureTypeScriptPerformance

Building Scalable React Architecture: A Production-Grade Guide

A deep dive into folder structures, state management patterns, code-splitting strategies, and performance budgets for large React applications.

Salem Shah··9 min read

Building Scalable React Architecture: A Production-Grade Guide

React's flexibility is both its superpower and its biggest trap. Without deliberate architectural decisions, a codebase that starts clean quickly becomes a maze of circular imports, prop-drilling, and re-render waterfalls. This article distils the patterns I use in production to keep large React applications maintainable at scale.

The Problem with "Just Start Building"

Most tutorials show you how to use React. Very few show you how to structure a React application for a team of five working on it for two years. The difference shows up at around 20,000 lines of code, when:

  • Onboarding a new developer takes three weeks instead of three days
  • Every feature requires touching five unrelated files
  • Shared components grow so many props they become untestable
  • Global state is read and written by dozens of components with no clear ownership

Folder Structure: Feature-Driven, Not Layer-Driven

Avoid the classic components/, hooks/, utils/, services/ split at the root. It scales poorly because it groups files by technical type rather than by business domain.

src/
  features/
    auth/
      components/     LoginForm.tsx, AuthGuard.tsx
      hooks/          useAuth.ts, useSession.ts
      api/            auth.api.ts
      store/          auth.slice.ts
      index.ts        (public API — only export what other features need)
    dashboard/
      components/
      hooks/
      index.ts
  shared/
    ui/               Button, Input, Modal — pure presentational
    lib/              date formatters, validators
    api/              base fetch wrapper
  app/                Next.js App Router pages and layouts

The index.ts barrel at each feature boundary is critical. It creates an explicit public API. If a component is not exported there, it is internal to the feature and cannot be imported by others. This enforces encapsulation without any build tooling.

State Management: Own Your Data at the Right Layer

The most common mistake I see is reaching for a global state manager before asking "does this state actually need to be global?".

| State type | Right home | |---|---| | Server data (users, posts) | React Query / SWR | | URL state (filters, page) | URL search params | | UI state (modal open, tab) | useState in the component | | Cross-feature shared state | Zustand slice / Redux slice | | Form state | React Hook Form |

Keeping server state in React Query eliminates 80% of the isLoading, error, data boilerplate that bloats global stores.

Performance: Preventing Death by a Thousand Re-Renders

Colocate State

A useState at the top of a 2,000-line component file triggers a re-render of the entire tree on every change. Move state as close to the leaf component that needs it as possible.

Memoisation Discipline

useMemo and useCallback are not free. Every call allocates a closure and runs a dependency comparison on every render. Only reach for them when:

  1. The value is passed to a React.memo child, or
  2. The computation is measurably expensive (profile first).

Virtualise Long Lists

react-virtual (TanStack Virtual) renders only the visible rows of a list. A 10,000-row table with virtualisation renders in under 16 ms. Without it, you're asking the browser to lay out DOM nodes that are off-screen.

Technical Decisions and Trade-offs

| Decision | Trade-off | |---|---| | Feature-driven folders | More directories, but trivial to delete a feature | | React Query for server state | Adds a dependency, eliminates manual loading/error state | | URL for filter state | Requires URL encoding logic, but gives free deep-linking | | Barrel index.ts boundaries | Requires discipline, prevents accidental coupling |

Scaling Strategy

As the application grows beyond 50,000 lines:

  1. Module Federation (Webpack 5) — split the app into independently deployable micro-frontends
  2. Turbopack / Vite — replace webpack for sub-second HMR
  3. Monorepo (Turborepo) — share the shared/ layer across multiple apps
  4. Component testing at the feature level — Storybook + Vitest covers 90% of UI bugs without E2E cost

Lessons Learned

  • The hardest architectural decision is not which library to use — it's agreeing on where the boundary between features lives.
  • Refactoring a folder structure later is cheap. Untangling circular state dependencies is expensive.
  • Write the index.ts public API first. It forces you to design the interface before the implementation.

Questions or corrections? Open an issue on GitHub or reach me at salemshahdev@gmail.com.