Skip to content

Blog

The React Folder Structure Nobody Teaches Juniors

Three practical ways to organize a React app (traditional, feature-based, and hybrid), with examples, comparison tables, and honest tradeoffs for real projects.

The React Folder Structure Nobody Teaches Juniors

Most React tutorials stop at the happy path.

You get a src/ folder. You drop components in components/. Pages go in pages/. Maybe you add hooks/ and utils/ because the tutorial said so. You ship a todo app. Everything feels fine.

Then the app grows. Or you join a team. And suddenly nobody knows where anything lives.

A billing button imports a hook from auth. API calls show up inside random components. Your components/ folder has 80 files and names like FinalButton2.tsx. Onboarding a new dev takes a week of searching.

That is not a React problem. That is a structure problem.

This article walks through three folder layouts people actually use:

  1. Traditional (type-based): components, pages, hooks, utils, services
  2. Feature-based: code grouped by product area (auth, billing, teams)
  3. Hybrid: features for product code, shared folders for real reuse

No perfect structure exists. But picking one on purpose beats letting the repo rot.

Why this matters

Production React is not hard because JSX is confusing.

It is hard because change spreads.

Fix one screen, break another. Add one API field, update four parsers. Share a hook too early and two features get tangled forever.

Good structure gives you boundaries. You should know:

  • who owns a file
  • who is allowed to import it
  • where to look when product asks for a change at 11pm

That is what the rest of this post is about.


1. Traditional structure (type-based)

This is what most tutorials teach. Files are sorted by technical type, not by product feature.

src/
├── app/              # or App.tsx at the root
├── pages/
├── components/
├── hooks/
├── utils/
├── services/
├── types/
└── assets/

Example: adding a "Teams" feature

With this layout, one feature gets split everywhere:

  • pages/TeamsPage.tsx
  • components/TeamCard.tsx
  • components/TeamList.tsx
  • hooks/useTeams.ts
  • services/teamApi.ts
  • utils/formatTeamName.ts

That is six folders for one idea. Now add auth, billing, comments, and admin. Your components/ folder becomes a graveyard.

When it works

Works well for

Best for
Small apps, prototypes, tutorials, hackathon demos
Also good when
The team is tiny (1–2 devs) and the scope is narrow
Team size
Solo or very small
Time to ship v1
Fast

Watch out for

Not great for
SaaS products with many domains (auth, billing, teams, admin)
Pain shows up when
More than ~15–20 screens or more than 2 people touching the same folders
Common failure mode
Giant components/ folder, unclear ownership, scary refactors

Honest take

There is nothing wrong with this layout for a weekend project.

The mistake is keeping it after the app becomes a product. Tutorials never stay alive long enough to rot. Your job does.


2. Feature-based structure

Here you group code by product feature (a slice of behavior users and devs can name).

Shared stuff still exists. But most code lives under features/.

src/
├── app/              # routes, providers, router wiring
├── assets/
├── components/       # truly shared UI (Button, Modal, Layout)
├── config/
├── features/           # most product code lives here
├── hooks/              # shared hooks only
├── lib/                # axios client, query client, etc.
├── types/
└── utils/

Each feature is a mini app:

src/features/discussions/
├── api/
├── components/
├── hooks/
├── types/
└── utils/

A small feature might only need components/ and api/. That is fine. Grow folders when you need them, not before.

Diagram of app, features, and shared folders in a feature-based React layout
Most product code lives under features/. Shared folders stay thin.

Example: same Teams feature

Everything team-related sits together:

src/features/teams/
├── api/
│   ├── getTeams.ts
│   └── useTeams.ts
├── components/
│   ├── TeamCard.tsx
│   └── TeamList.tsx
├── utils/
│   └── formatTeamName.ts
└── types/
    └── team.ts

The route in app/ imports from features/teams and composes the page. One place to open when product says "teams is broken."

When it works

Works well for

Best for
SaaS, dashboards, multi-module products
Also good when
Multiple devs work in parallel on different areas
Team size
Small to mid-size teams
Inspired by
Guides like Bulletproof React

Watch out for

Not great for
Tiny demos, single-screen apps, throwaway prototypes
Cost
More upfront thinking, need import rules
Common failure mode
Features importing each other (hidden coupling)

Two rules that make this stick

Rule A: features do not import from other features.

If billing needs auth, compose both at the app/route layer. Do not reach into another feature's folder.

Rule B: code flows one way.

  • Shared (components/, utils/, lib/) → can be used anywhere
  • Features → can use shared, not app/
  • App/routes → can use features and shared

Shared code must never import from a feature. If your generic Button imports from features/checkout, it is not shared anymore.

Some teams enforce this with ESLint import/no-restricted-paths. Strict, but it prevents Friday night surprises.


3. Hybrid structure (features + traditional shared layers)

This is the one most real teams land on over time.

You keep feature folders for product code, but you also keep the classic top-level folders for things that are genuinely cross-cutting.

src/
├── app/
├── pages/              # optional if you prefer pages/ over app/routes
├── components/         # design system + layout only
├── hooks/              # shared hooks only
├── utils/
├── services/           # shared API clients, auth header helpers
├── types/
├── features/
│   ├── auth/
│   │   ├── components/
│   │   ├── hooks/
│   │   └── api/
│   ├── billing/
│   └── teams/
└── assets/

How to decide where code goes

Use this checklist:

  1. Used by one feature only? → put it in that feature's folder
  2. Used by 2+ features with the same API? → promote to shared (components/, hooks/, utils/)
  3. Wires multiple features together? → route/page in app/ or pages/
  4. Third-party wrapper (Router Link, Radix Dialog)? → shared components/ui/

Example: team settings page

Team settings needs teams and permissions:

src/app/routes/TeamSettingsPage.tsx   # composes both features
src/features/teams/...              # team UI + data
src/features/auth/...                 # role checks
src/components/ui/...                 # Button, Card (shared)

The page imports from both features. The features do not import each other.

When it works

Works well for

Best for
Growing products moving from "tutorial layout" to something maintainable
Also good when
You want feature boundaries but already have services/ and hooks/ habits
Team size
Small teams scaling up, or mid-size teams standardizing

Watch out for

Not great for
If nobody agrees on the promotion rules (everything still lands in components/)
Risk
Two systems at once unless rules are clear
Fix
Write 5 lines in the README: "new product code → features/"

Honest take

Hybrid is not laziness. It is pragmatism.

You get feature ownership and a familiar place for shared utilities. Just do not let components/ become the default dump again.


Compare the three (interactive)

Tap a tab to see the same app organized three different ways.

src/
├── pages/
│   ├── LoginPage.tsx
│   ├── TeamsPage.tsx
│   └── BillingPage.tsx
├── components/
│   ├── TeamCard.tsx
│   ├── InvoiceTable.tsx
│   └── LoginForm.tsx
├── hooks/
│   ├── useAuth.ts
│   └── useTeams.ts
├── services/
│   ├── authApi.ts
│   └── billingApi.ts
└── utils/
    └── formatDate.ts

Teams change? Search across pages, components, hooks, and services.

Side-by-side summary

Traditional Feature-based Hybrid
Sorts by File type Product area Product area + shared layers
Best for Demos, tiny apps Multi-feature SaaS Teams growing out of flat folders
Worst for Large long-lived products One-screen throwaways Teams with no import rules
Finding code Global search Open the feature folder Feature first, then shared
Onboarding Easy at first, hard later Clear after a short README Needs promotion rules documented
Refactor cost High when big Lower per feature Medium

Shared components: a rule that saves arguments

Juniors put everything in components/ because "we might reuse it."

Better rule:

Start inside the feature. Move to shared when reuse is real, not imagined.

UserAvatar on the billing page? Keep it in features/billing/components/ until a second feature actually needs it. Then promote it with a real API.

Shared components/ should be your design system: buttons, inputs, dialogs, layout shells. Not every card you ever built.


API calls: one home, not three

Pick one pattern and stick to it:

  • Per-feature API (features/foo/api/) when endpoints belong to one domain
  • Shared services/ when many features hit the same client or auth layer

The bad option is all three at once: some calls in services/, some in features/auth/api, some inline in a component because someone was rushing.

If you use React Query, keep query keys, fetchers, and hooks together. When the backend changes the discussions endpoint, you should not grep the whole repo.


Barrel files (index.ts): small tip

Barrel files looked clean:

export * from './components';
export * from './api';

Pretty imports. Sometimes ugly bundles and circular dependency bugs.

These days many teams import from the exact file they need. Slightly longer imports. Healthier builds. Fewer surprises.


Already stuck with a flat mess?

You do not rewrite the repo on Monday.

  1. Stop the bleeding. New product code goes into features/. No new domain UI in root components/.
  2. Move one feature you are actively building. Accept messy imports for a sprint.
  3. Add one ESLint rule blocking cross-feature imports.
  4. Delete dead code while you move. Migrations are free cleanup.

Ugly progress beats two more years of a junk drawer.


Questions to ask before you create a file

Memorizing folder names does not help. These do:

  • Who owns this? (Which feature?)
  • Who can import it? (Shared, feature, or app layer?)
  • What if this feature doubles in size? (Still one folder?)
  • Can I delete this feature without touching half the repo? (If no, boundaries failed.)

React gives you freedom. Structure gives you sanity.


Closing thought

Bootcamps optimize for demos. Jobs optimize for maintenance.

A todo app does not need features/. A product with auth, billing, teams, and admin probably does.

If you are a solo dev or small team shipping a real React product, start with feature-based thinking. Use hybrid if you already rely on shared hooks/ and services/. Keep traditional for small scope and short life.

Your future self, the one debugging production at midnight, will not thank you for a perfect utils/ folder.

They will thank you for knowing exactly which folder to open.

← Back to all posts