How I Structure Large React Codebases in 2026

ReactFrontendArchitecture

Every large React codebase I've worked on started the same way. A components/ folder, a hooks/ folder, a utils/ folder. Clean. Flat. Sensible for a project with 20 files. And then the codebase grew. And grew. And at some point you're scrolling through 80 components in a single directory wondering where UserProfileCard ends and UserProfileCardSkeleton begins.

Getting React project structure right matters more than most teams realize. The wrong structure doesn't break the app. It quietly kills the team's velocity.

Why the Flat Structure Breaks Down

The classic structure looks like this:

src/
  components/
  hooks/
  utils/
  services/
  types/

It feels organized because the categories are logical. The problem shows up when you try to work on a feature. Say you're building a billing page. Your billing component is in components/BillingPage.tsx. The data fetching hook is in hooks/useBilling.ts. The API call is in services/billing.ts. The TypeScript types are in types/billing.ts.

You're jumping between four folders to work on one thing. When your codebase has 10 features, that's annoying. At 50 features, it's a real problem.

Feature-Based Organization: Colocate Everything

The fix is straightforward: organize by feature, not by type. Each feature owns everything it needs: components, hooks, services, types, tests. All in one place.

src/
  features/
    billing/
      components/
      hooks/
      services/
      types/
      index.ts
    auth/
      components/
      hooks/
      services/
      types/
      index.ts
  shared/
    components/
    hooks/
    utils/

The rule is simple: if a hook, component, or utility is used by only one feature, it lives inside that feature. If it gets used by two or more features, it moves to shared/.

Module Boundaries and the Index File

Feature folders should expose a public API through their index.ts. Internal implementation files stay private to the feature.

// features/billing/index.ts
export { BillingPage } from './components/BillingPage';
export { BillingCard } from './components/BillingCard';
export { useBilling } from './hooks/useBilling';
export type { BillingRecord, BillingStatus } from './types/billing.types';

You can enforce this with an ESLint rule. The no-restricted-imports rule with a pattern like @/features/*/components/* will catch anyone reaching past the index.ts. Add this to your ESLint config and it becomes automatic.

The Barrel File Debate

This is the one where I'll give you my actual opinion: stop using barrel files in application code.

When you import from a barrel, JavaScript loads every module that barrel re-exports, including the ones you don't need. I've seen projects go from 11,000 modules loaded per page down to 3,500 after removing barrel files. That's a 68% reduction.

That said, barrels have one legitimate use: the public API of a package or feature module. The rule I follow is one barrel per module boundary, no barrels inside a module.

Naming Conventions That Scale

A few rules I won't compromise on:

  • Component files match component names. UserProfileCard.tsx exports UserProfileCard.
  • Hooks start with use. Always. useUserProfile, not userProfile or profileHook.
  • API files end in .api.ts. billing.api.ts, users.api.ts.
  • Type files end in .types.ts. One file per feature domain.
  • Test files live next to the code. BillingCard.test.tsx in the same folder as BillingCard.tsx.

Getting There From Where You Are

If you're looking at an existing flat codebase and wondering where to start, my advice is to not do a big rewrite. Pick one new feature that's about to be built, apply the feature-based structure to it, and get the team used to the pattern. Then migrate existing code opportunistically when you're touching it anyway.

The architecture pays back over months, not weeks. What matters is getting the boundaries right now so you're not untangling them later.