Back to Blog
Lesson 19 of the Advanced React: Performance, Architecture & Patterns course
ReactJune 27, 20263 min read

Mastering Modular Directory Structures in React for Scalability

Stop drowning in a sea of components. Learn to implement feature-based directory structures, use barrel files, and manage dependencies for scalable React apps.

ReactArchitectureScalabilityProject StructureModularityjavascriptfrontend

Previously in this course, we explored State Colocation Strategies to keep logic close to where it’s used. Today, we scale that concept to the entire codebase by organizing our files into feature-based modules.

If you’ve ever spent ten minutes searching for the right file in a massive /components directory, you’ve felt the pain of "type-based" organization. When your project grows, grouping files by their technical role (e.g., all hooks here, all components there) leads to high cognitive load and "spaghetti" dependencies.

The Case for Feature-Based Architecture

In a type-based structure, files related to a single feature—say, User Authentication—are scattered across /hooks, /components, /services, and /types. To change one feature, you jump across four different sub-trees.

A feature-based Project Structure groups files by the domain they serve. This improves Scalability and Modularity by creating clear boundaries. If you need to delete or move a feature, you simply move one folder.

The Anatomy of a Feature Folder

Each feature should be self-contained. A typical features/auth directory looks like this:

TEXT
src/features/auth/
├── api/          # API calls (e.g., login.ts)
├── components/   # Feature-specific components (e.g., LoginForm.tsx)
├── hooks/        # Feature-specific hooks (e.g., useAuth.ts)
├── types/        # TypeScript interfaces for this feature
└── index.ts      # The Public API (Barrel File)

Implementing Barrel Files for Encapsulation

The secret to managing Architecture boundaries is the "Barrel File" (index.ts). A barrel file acts as a gatekeeper, determining which parts of your feature are public and which are private.

By importing only what is exported in the index.ts, you enforce a "black box" design. If a file is not in the barrel, other features shouldn't touch it.

Worked Example: The Auth Module

Let’s define a clean API for our auth feature.

features/auth/index.ts

TYPESCRIPT
// Explicitly expose only what other modules need
export { LoginForm } from CE9178">'./components/LoginForm';
export { useAuth } from CE9178">'./hooks/useAuth';
export type { User } from CE9178">'./types';

features/auth/components/internal/AuthLogger.ts (Private)

TYPESCRIPT
// This file is NOT exported in index.ts. 
// Other modules cannot import this directly.
export const logAuthEvent = () => console.log(CE9178">'Auth event');

By keeping internal logic inside the folder but outside the barrel file, you prevent accidental coupling. If you need to refactor the internal logger, you know exactly what will break because you control the exports.

Managing Cross-Module Dependencies

A common pitfall in large apps is circular dependencies. Module A imports from Module B, which imports from Module A. To avoid this, we follow a strict hierarchy:

  1. Features can import from Shared (UI kits, utilities).
  2. Features can import from other Features only via their public index.ts.
  3. Shared code must NEVER import from Features.

If two features need to share logic, extract that logic into a new, smaller feature or a shared utility package.

Hands-on Exercise: Refactoring your Structure

  1. Identify a Feature: Pick one domain in your current project (e.g., checkout, profile, or dashboard).
  2. Create the Folder: Move all associated components, hooks, and types into a new src/features/<name> directory.
  3. Define the Barrel: Create an index.ts in that folder and export only the necessary components/hooks.
  4. Fix Imports: Update all files that reference these components to use the new path (e.g., import { LoginForm } from '@/features/auth').

Common Pitfalls

  • The "Everything is Public" Trap: Exposing every single file in your barrel file defeats the purpose. Keep internal utilities private.
  • Deep Nesting: While feature-based, try to keep your directory depth to 3-4 levels maximum. If you find yourself needing more, your feature is likely too large and should be split.
  • Forgetting the @ Alias: Use TypeScript path aliases (e.g., @/features/auth) to avoid "import hell" like ../../../features/auth.

Recap

Organizing by feature rather than by file type is the first step toward a Modular codebase. By implementing barrel files, you create a clear contract between modules, making your code easier to reason about and refactor. As we continue to optimize our project, these boundaries will be critical when we look at Route-level Code Splitting in a future lesson.

Up next: We'll dive into Refactoring Monolithic Components to break down the massive files that still exist within our new folders.

Similar Posts