DR

Structuring Large React Applications: A Practical Guide

Aug 12, 2026
7 min read

When you start a new React project, maintaining an intuitive folder structure feels effortless. A standard setup with /components, /hooks, /pages, and /utils works beautifully for a personal side project or an MVP with half a dozen routes.

However, as the application growing to 50+ pages, hundreds of components, and multiple engineering teams working in parallel, the initial setup often collapses under its own weight. Developers face to modifying five different top-level folders to make a simple change to a single feature, circular dependency hell, and continuous friction during code reviews.

Over years of building and scaling enterprise-grade React and Next.js applications, I have converged on a predictable, modular, and maintainable architecture designed to scale seamlessly with team size and feature complexity.

Here is a comprehensive breakdown of how I structure a large-scale React application.


1. The Core Philosophy: Feature-First over Layer-First

The fundamental shift in scaling a React application is transitioning from a layer-first organization to a feature-first (domain-driven) organization.

Layer-First (The Monolithic Approach)

Text
src/
├── components/
│   ├── UserProfile.tsx
│   ├── OrderHistory.tsx
│   ├── Navbar.tsx
├── hooks/
│   ├── useUser.ts
│   ├── useOrders.ts
├── services/
│   ├── userService.ts
│   ├── orderService.ts
└── types/
    ├── user.ts
    └── order.ts
  • The Problem: Related code is scattered across the entire repository. To update the user profile feature, you must jump across four different directories. When it comes time to delete, refactor, or test a feature, isolating its dependencies becomes a tedious scavenger hunt.

Feature-First (Domain-Driven Architecture)

Text
src/
├── features/
│   ├── user-profile/
│   ├── order-management/
│   └── authentication/
  • The Solution: Keep code that changes together co-located. A feature folder encapsulates all domain logic, components, state management, and type definitions required for that specific business domain.

2. Directory Structure Blueprint

Here is the full structure of a production-ready React codebase:

Text
src/
├── app/                  # Application routes, entry points, layouts, and global providers
│   ├── routes/           # Router configuration (e.g., React Router or Next.js App Router)
│   └── provider.tsx      # Global Context Providers wrapper (QueryClient, Auth, Theme)
│
├── assets/               # Static assets (fonts, global images, SVG icons)
│
├── components/           # Generic, domain-agnostic UI components (Design System / UI Kit)
│   ├── ui/               # Primitive components (Button, Modal, Input, Badge, Table)
│   └── feedback/         # Toast notifications, Loaders, Error boundaries
│
├── config/               # Application configuration constants, env validation, third-party keys
│   ├── env.ts            # Type-safe environment variable parsing (e.g., Zod env validation)
│   └── constants.ts      # Global application constants
│
├── features/             # Business domain modules (Domain-Driven core)
│   ├── authentication/   # Self-contained feature module
│   │   ├── api/          # Query/Mutation hooks and API request calls specific to Auth
│   │   ├── components/   # UI components specific to Auth (e.g., LoginForm, SignupCard)
│   │   ├── hooks/        # Custom hooks specific to Auth domain
│   │   ├── stores/       # Local state stores (if applicable)
│   │   ├── types/        # TypeScript interfaces and types for Auth
│   │   ├── utils/        # Domain-specific helpers (e.g., password strength validation)
│   │   └── index.ts      # Public API boundary for the feature module
│   │
│   └── billing/          # Another isolated feature module
│
├── hooks/                # Global, reusable custom hooks (e.g., useDebounce, useMediaQuery)
│
├── lib/                  # Pre-configured third-party instances (Axios, TanStack Query, Analytics)
│   ├── api-client.ts     # Configured Axios / Fetch wrapper with interceptors
│   └── react-query.ts    # React Query client defaults
│
├── stores/               # App-wide global UI state stores (e.g., Zustand UI store, Theme)
│
├── types/                # Shared global TypeScript types and generic utility types
│
└── utils/                # Global helper functions (formatting dates, currency, string manipulation)

3. Key Architectural Principles & Enforcements

Rule 1: Enforce Strict Module Boundaries (The Public API Pattern)

Every folder inside src/features/* should behave like an internal npm package with a single index.ts file acting as its public gateway.

src/features/authentication/index.ts

TypeScript
// Explicitly expose only what other parts of the app are allowed to use
export { LoginForm } from './components/LoginForm';
export { useAuth } from './hooks/useAuth';
export type { User, AuthStatus } from './types';

Why this matters:

  • Encapsulation: Internal sub-components (like PasswordStrengthMeter.tsx or AuthHeader.tsx) remain private to the authentication feature.
  • Refactoring Freedom: You can restructure internal files within a feature without breaking imports across the rest of the application.
  • Preventing Spaghetti Dependencies: Cross-feature imports must go through the public interface (@/features/authentication).

Rule 2: Separate UI, Logic, and Data Fetching

Keep components lean by extracting data fetching and side effects into custom hooks.

BAD: Everything in one monolithic component

TSX
// ❌ Bloated component handling UI, API call, and state
export const UserProfileCard = ({ userId }: { userId: string }) => {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
 
  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then((res) => res.json())
      .then((data) => {
        setUser(data);
        setLoading(false);
      });
  }, [userId]);
 
  if (loading) return <Spinner />;
  return <div>{user?.name}</div>;
};

GOOD: Clean separation of concerns

  1. Custom Hook for Data Fetching (src/features/user-profile/api/use-user.ts):
TypeScript
import { useQuery } from '@tanstack/react-query';
import { apiClient } from '@/lib/api-client';
import { User } from '../types';
 
export const useUser = (userId: string) => {
  return useQuery<User>({
    queryKey: ['user', userId],
    queryFn: () => apiClient.get(`/users/${userId}`).then((res) => res.data),
  });
};
  1. UI Component (src/features/user-profile/components/UserProfileCard.tsx):
TSX
import { Spinner } from '@/components/ui/Spinner';
import { useUser } from '../api/use-user';
 
export const UserProfileCard = ({ userId }: { userId: string }) => {
  const { data: user, isLoading, error } = useUser(userId);
 
  if (isLoading) return <Spinner />;
  if (error || !user) return <div>Failed to load profile.</div>;
 
  return (
    <div className="p-4 border rounded-lg shadow-sm">
      <h3 className="text-lg font-bold">{user.name}</h3>
      <p className="text-sm text-gray-600">{user.email}</p>
    </div>
  );
};

4. Pragmatic State Management Hierarchy

A common pitfall in large React applications is treating all state equally. A robust architecture separates state into four distinct tiers:

State TierRecommended ToolPurpose & Example
Server StateTanStack Query / SWRAPI response caching, revalidation, optimistic updates
Local UI StateuseState, useReducerModal visibility, form field toggles, dropdown open/closed
Global UI StateZustand / JotaiSidebar open state, global user preferences, persistent active theme
URL StateuseSearchParams / nuqsSearch queries, pagination page numbers, table filters (deep-linkable)

Pro Tip: Keep 80–90% of your data layer in Server State using a tool like TanStack Query. Avoid duplicating API responses in global Redux/Zustand stores.


5. Type-Safe Infrastructure & Alias Imports

Configure TypeScript path aliases to eliminate messy relative imports like ../../../../components/ui/Button.

tsconfig.json

JSON
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/app/*": ["src/app/*"],
      "@/components/*": ["src/components/*"],
      "@/config/*": ["src/config/*"],
      "@/features/*": ["src/features/*"],
      "@/hooks/*": ["src/hooks/*"],
      "@/lib/*": ["src/lib/*"],
      "@/stores/*": ["src/stores/*"],
      "@/types/*": ["src/types/*"],
      "@/utils/*": ["src/utils/*"]
    }
  }
}

This enforces consistent import paths regardless of file depth:

TypeScript
// Clean, predictable, refactor-friendly
import { Button } from '@/components/ui/Button';
import { useAuth } from '@/features/authentication';
import { apiClient } from '@/lib/api-client';

Summary & Checklist for Refactoring

If you are looking to scale your current React codebase or restructure a legacy monolith, use this simple checklist:

  • Are components grouped by domain/feature rather than file type?
  • Do feature folders expose a public index.ts boundary?
  • Are shared design system components strictly domain-agnostic?
  • Is server state decoupled from global client state using TanStack Query or SWR?
  • Are import paths standardized using TypeScript path aliases?

A well-structured codebase is not about strict dogmatism—it is about lowering cognitive load, enabling parallel development, and making wrong implementations hard to write.

Tagged with
ReactArchitectureBest Practices
Share this article

© 2026 Dilshan Ramesh. All Rights Reserved.