How I Structure React Components for Teams of 10+

Folder structure is the first architectural decision a React project makes and the last one anybody revisits. Get it roughly right and new features have an obvious home for years; get it wrong and every developer invents their own convention until the codebase has four. This is the structure I have converged on across projects of three to ten-plus engineers, including what changed as teams grew and which popular conventions I dropped.
Feature folders over type folders
When more than a handful of developers touch the same repository, organizing by file type (components/, hooks/, utils/) scales poorly. New features sprawl across directories and imports become a maze. I standardize on feature-based folders: each domain owns its components, hooks, API adapters, and tests. Shared primitives live in a thin ui layer. That structure makes ownership obvious in code review and limits merge conflicts.
What does a good React folder structure look like?
The concrete shape, for a mid-sized application:
src/
features/
checkout/
components/
CheckoutSummary.tsx
PaymentForm.tsx
PaymentForm.test.tsx
hooks/
useCheckoutTotals.ts
api/
checkoutApi.ts
types/
order.ts
utils/
formatCurrency.ts
index.ts only this is importable from outside
catalogue/
account/
shared/
ui/ Button, Modal, Input, DataTable
hooks/ useDebounce, useMediaQuery
lib/ apiClient.ts, analytics.ts
types/ ApiResult, Paginated
app/
routes.tsx
providers.tsx
store.tsThree rules make it hold under a growing team, and they matter far more than the exact folder names:
- Tests live beside the code they test. A test three directories away gets deleted during a refactor; a test next to the file gets updated.
- Features import from other features only via
index.ts. Reaching into another feature's internals is what turns modules into a single tangled unit, and it is trivially enforceable with an ESLint boundary rule. shared/requires a second consumer. Something is promoted when a second feature genuinely needs it, not when someone anticipates that one might.
A concrete variant of this structure, with the domain specifics of a nine-developer healthcare project, is in building a hospital ERP frontend — same principles, different constraints.
What naming conventions are worth enforcing?
Naming is where consistency pays disproportionately, because it is what developers scan rather than read. What I standardise:
- Components:
PascalCase.tsx, one component per file, file named for the component.PaymentForm.tsxexportsPaymentForm— noindex.tsxfiles named after their folder, which make editor tabs a wall of identical labels. - Hooks:
useThing.ts, named for what they return or do, not how they work.useCheckoutTotalsbeatsuseCalculation. - Handlers:
handleXinside a component,onXas a prop. The distinction tells a reader instantly which side of the boundary a function belongs to. - Booleans read as assertions:
isLoading,hasError,canSubmit. A boolean calledsubmitcosts someone a lookup. - Types: singular nouns, no
Iprefix.Order, notIOrder;OrderSummary, notOrderSummaryInterface. - Test files mirror the source name —
PaymentForm.test.tsxnext toPaymentForm.tsx.
None of these is objectively correct. Their value is entirely in being applied uniformly, which means they belong in a written document with one canonical example each, not in reviewers' heads.
When should a component move to shared?
The rule I apply is the rule of three, adapted: build it in the feature that needs it; when a second feature needs it, copy it; when a third needs it, extract it.
That deliberate duplication at step two feels wrong and is almost always right. Two similar components usually diverge — the second consumer wants a different empty state, an extra prop, slightly different spacing — and the divergence is only visible after both exist. Extracting at the first sign of similarity produces the component with eleven boolean props that everyone is afraid to change.
The signals that something genuinely belongs in shared/ui:
- It has no knowledge of any domain — a
Buttondoes not know what an order is - Its props are about presentation and behaviour, not business rules
- Three or more features use it, or it is part of a deliberate design system
- Changing it for one caller would not obviously break another
And the counter-signal: if extracting it requires adding a variant prop whose branches share almost no code, you have two components wearing one name.
Compound components for flexible APIs
Complex widgets — modals, menus, data tables — benefit from compound components: a parent exports named subcomponents that share implicit context. Consumers compose markup without prop drilling twenty booleans. Documenting the pattern once in Storybook or a short README prevents one-off forks of the same UI.
Hooks as the business logic seam
I push data fetching, normalization, and side effects into hooks with explicit names (useProjectFilters, useInvoiceTotals). Presentational components receive ready-to-render data. That split speeds up testing and lets backend contract changes touch fewer files.
Context, Zustand, and props
Context works for stable, low-frequency data like theme and auth session. For high-frequency updates or cross-route state, a small Zustand store with selectors avoids rerender storms. I reserve prop drilling for strictly local trees where the data flow is obvious in under three levels.
Are barrel files worth it?
Partly. This is the convention I have changed my mind about most.
A barrel file — an index.ts re-exporting a folder's contents — gives you tidy imports and a deliberate public surface. The cost is real though: barrels defeat tree-shaking in some bundler configurations, so importing one component can pull in the whole folder; they create circular-import problems that produce baffling undefined errors at runtime; and they slow down TypeScript and hot reload on large codebases, because touching one file invalidates everything importing through the barrel.
My compromise, which has survived several projects:
- One barrel per feature, at the feature root.
features/checkout/index.tsexports the handful of things other features may use. This is the boundary that earns its keep — it is the enforcement point for the import rule above. - No barrels inside a feature. Components import siblings by direct path. The tidiness gain is small and the circular-import risk is where it actually bites.
- No barrel for
shared/ui. Importshared/ui/Buttondirectly. This is the folder most likely to be imported everywhere, so it is exactly where bundling and rebuild costs concentrate.
If you inherit a codebase with a giant root barrel and mysterious undefined imports at startup, that is nearly always a circular dependency running through it.
How did the structure change from three developers to ten?
The structure above is the end state. It did not start there, and the changes were driven by pain rather than planning.
At three developers, we had type folders — components/, hooks/, utils/ — and it was genuinely fine. Everyone had the whole codebase in their head, and finding things was never the bottleneck. Anyone advising a three-person team to adopt heavy module boundaries is selling ceremony.
At five, the seams appeared. components/ had grown past sixty files with no grouping, two developers wrote near-identical modals in the same fortnight, and merge conflicts clustered in the shared folders everyone touched. This is the moment to reorganise — the codebase is still small enough to move in a day, and the pain is now legible enough that the team agrees it is worth doing.
At seven, ownership became the problem rather than navigation. Feature folders existed, but nothing stopped one team importing another's internals, so a refactor in one area broke three others. Fixes: the index.ts boundary rule, an ESLint restriction enforcing it, and named owners per feature.
At ten-plus, the constraint moved to the shared layer. Every team needed changes to shared/ui, and every change risked someone else's screen. Fixes: explicit ownership of shared components, a rule that breaking changes update all consumers in the same PR, and visual regression tests on the shared components specifically.
The pattern across all four steps: reorganise one stage after it starts hurting, not one stage before. Every premature structure I have introduced was wrong in a way we then had to unwind, because we were guessing at problems we had not met.
What went wrong: the reorganisation that stalled
The failed version of this was a "restructure to feature folders" PR I opened at around the six-developer mark. It moved roughly four hundred files in one commit. It was technically correct and it never merged.
Nobody could review it, so it sat for a week collecting rebases against a codebase five other people were actively changing. After the third full-repository rebase we closed it. Two months later we did the same reorganisation successfully — one feature at a time, over about five weeks, with the old structure and the new one coexisting the whole way. Total effort was higher; the amount that actually shipped went from zero to all of it.
What I would do differently is treat structural change as a migration rather than a refactor: incremental, reviewable, and safe to abandon halfway. The tell that you are about to repeat my mistake is a PR description containing the phrase "just moves files" — that phrase is doing an enormous amount of work, and reviewers are the people who pay for it.
JSDoc where it matters
Not every function needs an essay. I add JSDoc to exported hooks and public component props: parameters, return shape, and invariants other teams must respect. That investment pays off when onboarding developers who will not grep the implementation on day one.
Closing thought
Architecture is a team contract. Folder layout, component APIs, and state boundaries should make the right change easy and the wrong change visible in review. That is how you keep a React codebase legible at ten-plus engineers.
I applied exactly these patterns while scaling a hospital ERP team from 3 to 9 developers — the full story is in building a hospital ERP frontend with a 9-developer team.
Muzamal Ali — Senior Frontend Engineer & Team Lead
Senior Frontend Engineer with 5+ years building production React and Next.js applications. I've led teams of 3–9 developers across healthcare, aviation, AI, and SaaS platforms. Based in Pakistan, working async with European tech teams.
Working on something similar?
I help European tech teams ship better frontends.


