How I Structure Large React Codebases in 2026
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.tsxexportsUserProfileCard. - Hooks start with
use. Always.useUserProfile, notuserProfileorprofileHook. - 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.tsxin the same folder asBillingCard.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.