,

How to structure a large React project so it doesn’t collapse under its own weight

Most React projects don’t fail because of a bad library choice. They fail because nobody enforced a folder structure, and eighteen months later every file imports from every other file. Fixing that later costs weeks. Setting rules up front costs an afternoon.

A structure that scales

Group by feature, not by file type. A folder full of forty “components” with no relationship to each other is harder to navigate than ten feature folders that each own their own components, hooks, and API calls.

src/
  app/                # routing, providers, app shell
    routes/
    providers/
  features/
    auth/
      components/
      hooks/
      api.ts
      types.ts
      index.ts
    billing/
      components/
      hooks/
      api.ts
      types.ts
      index.ts
  shared/
    components/       # generic, reusable UI (Button, Modal, Input)
    hooks/             # generic hooks (useDebounce, useLocalStorage)
    lib/               # fetch wrapper, date utils, formatting
    types/
  assets/

The dependency rules that actually matter

  • Features never import from other features directly. If billing needs something from auth, that logic belongs in shared, or you’re exposing the wrong boundary.
  • Shared code never imports from features. Dependencies flow one direction: shared to features to app. Never the reverse.
  • Each feature exposes one public entry point. An index.ts that re-exports only what other parts of the app are allowed to touch. Everything else inside the folder is private.
  • No file over roughly 300 lines. Not a hard rule, but a strong signal that a component or hook is doing too much and needs splitting.

Where state actually lives

Keep state as close as possible to where it’s used. Local UI state stays in the component. State shared across a feature lives in that feature’s hooks. State shared across the whole app — auth session, theme, current user — is the only kind that belongs in a global store.

Enforce it, don’t just document it

  • Use ESLint’s import/no-restricted-paths or a similar rule to physically block cross-feature imports at lint time, not just in a wiki page nobody reads.
  • Add a barrel file check so features can’t be imported from anywhere except their index.ts.
  • Review folder structure in code review the same way you’d review logic — a misplaced file is technical debt just like a bad function.

When to break the rules

Small apps and prototypes don’t need this. If you have fewer than ten components and one developer, feature folders are overhead. Introduce structure when the second developer joins or the third feature ships — not before, not much after.

If your team is also experimenting with AI coding agents to speed up refactors like this one, see how to build a serious dev workflow around Claude — a clean folder structure makes agent-driven refactors far more reliable, since the agent can reason about boundaries instead of guessing.

Quick FAQ

Should I use absolute imports or relative imports?

Absolute imports (via tsconfig path aliases) once you have more than two or three folder levels. Relative imports past ../../../ are a readability tax with no upside.

Do I need a monorepo for this to work?

No. Feature-based structure works fine inside a single app. Monorepos help when multiple apps share code, which is a separate problem.

What about a “core” or “common” folder that keeps growing?

That’s a smell. If shared keeps absorbing feature-specific logic, it usually means a feature boundary was drawn in the wrong place — revisit it instead of adding more to shared.

Leave a Reply

Your email address will not be published. Required fields are marked *