This article is based on my personal experience and interpretation. The approach I'm sharing isn't the only correct answer, and my understanding of each architecture pattern may differ from the official definitions. I hope you'll read this as just one perspective among many.
Introduction
I've struggled with React project folder structure for a long time.
React, being a UI library, offers immense flexibility in how we structure our projects. However, this freedom often leads to a common dilemma: where should I put my code?
My Failure: Overcomplicating from the Start
I once read extensively about Clean Architecture and DDD (Domain-Driven Design), became determined thinking "I need to do this properly," and created a project with domain layer, useCase layer, infrastructure layer... all meticulously separated from day one.
These terms will be explained later, but roughly speaking, it's the idea of "separating code into layers by responsibility."
The result, at least in my case, was painful:
- File count grew excessively, couldn't find anything
- Even simple feature additions required jumping between multiple folders
- When teammates asked "why is it structured this way?", I couldn't explain it well
- Eventually, the design collapsed
The root cause was introducing complexity to prepare for problems that hadn't occurred yet.
A Gradual Approach
From this experience, I switched to a "start simple, add complexity when needed" approach. This approach shares some ideas with Feature-Sliced Design (FSD), but focuses on a more gradual, less prescriptive adoption path. In this article, I'll introduce this approach in 3 stages. It may be imperfect in some ways, but I hope it helps someone facing similar struggles.
Why Package by Feature + Clean Architecture?
My initial failure happened because I tried to apply Clean Architecture to the entire project. Forcing the same domain layer, useCase layer structure on every feature caused files to explode, making things unmanageable.
So I changed my thinking:
- Use Package by Feature (organizing by business feature) as the base
- Introduce Clean Architecture layers only to features that become complex
This combination solves:
| Approach | Problem When Used Alone | Solved by Combination |
|---|---|---|
| Clean Architecture only | Forces layers on all features → file explosion | Introduce layers only where needed |
| Package by Feature only | Dependencies within features are unclear | Layers clarify dependency direction |
In other words, because code is organized by feature, we can gradually add complexity to specific features only. This is the core of the approach I'm sharing.
Note: This article is not a "correct explanation" of each architecture. My interpretation might be wrong, and this approach isn't the only answer. This is simply a personal experience sharing of how I interpreted and adopted these patterns. Please refer to the original sources for accurate definitions. I apologize for any misinterpretations or inappropriate descriptions.
Why Think About Folder Structure? — Separation of Concerns
What Should UI Components Be Responsible For?
There's a school of thought that React components should focus on "how things look".
// A component that just receives props and displays them
const ProductCard = ({ name, price, onAddToCart }: Props) => (
<div className="card">
<h3>{name}</h3>
<p>${price.toLocaleString()}</p>
<button onClick={onAddToCart}>Add to Cart</button>
</div>
)
Components like this:
- Can be tested just by passing props
- Can be easily displayed in Storybook
- Can be reused on different screens
Dan Abramov once proposed the "Container/Presentational pattern" (separating data-fetching components from display components), but it's no longer necessary with Hooks. Dan himself noted in 2019 that "I don't suggest splitting your components like this anymore." However, what Dan was criticizing was the "separation pattern assuming class components," not the concept of "separation of concerns" itself. I believe separating UI from logic is still valid for testability and reusability.
A Common Anti-pattern
In reality, we tend to stuff various logic into components. The following is an extreme example, but many of us have probably written something similar:
// Everything stuffed into a component
const ProductCard = ({ productId }: Props) => {
const [product, setProduct] = useState(null)
const [isLoading, setIsLoading] = useState(true)
// API communication inside the component
useEffect(() => {
fetch(`/api/products/${productId}`)
.then(res => res.json())
.then(data => {
setProduct(data)
setIsLoading(false)
})
}, [productId])
// Business logic inside the component
const discountedPrice = product
? product.price * (1 - product.discountRate)
: 0
// Cart logic here too...
const handleAddToCart = async () => {
await fetch('/api/cart', {
method: 'POST',
body: JSON.stringify({ productId, quantity: 1 })
})
}
if (isLoading) return <Loading />
return (
<div className="card">
<h3>{product.name}</h3>
<p>${discountedPrice.toLocaleString()}</p>
<button onClick={handleAddToCart}>Add to Cart</button>
</div>
)
}
Problems with this component:
| Problem | Impact |
|---|---|
| API communication embedded | Need mocks for testing, doesn't work offline |
| Business logic mixed in | Need React to test discount calculation |
| Not reusable | Using elsewhere triggers another API call |
| Hard to display in Storybook | Need API mocks, state mocks |
| Unclear change impact | Want to change UI but have to touch logic |
Why Separation of Concerns Matters
To avoid these problems, Separation of Concerns becomes important.
❌ Not Separated ✅ Concerns Separated
┌─────────────────┐ ┌─────────────────┐
│ Component │ │ UI │
├─────────────────┤ └────────┬────────┘
│ - UI Display │ │
│ - API Comm │ ▼
│ - Biz Logic │ ┌─────────────────┐
└─────────────────┘ │ hooks │
└───────┬─┬───────┘
│ │
┌───────┘ └───────┐
▼ ▼
┌──────────┐ ┌──────────────┐
│ domain │ │infrastructure│
└──────────┘ └──────────────┘
- UI (View): Visual display, receiving user interactions
- Logic (Domain): Business rules, calculations, data transformation (e.g., discount calculation, tax-inclusive pricing)
- Data Fetching (Infrastructure): API communication, database connections, external service integration
By properly separating these, each can be tested, modified, and reused independently.
The folder structure in this article gradually achieves this "separation of concerns." As we progress from Stage 1 → 2 → 3, the separation becomes clearer.
Stage 1 Stage 2 Stage 3
───────────────── ───────────────── ─────────────────
feature/ feature/ feature/
├── Component.tsx ├── components/ ├── components/
├── useHook.ts ├── hooks/ ├── hooks/
└── utils.ts └── utils/ ├── domain/
└── useCases/
───────────────────────────────────────────────▶
Increasing Structure
Stage 1: Start by Organizing by Feature
Stage 1 is the "Package by Feature" approach. Instead of organizing by technical role (components, hooks, etc.), code is grouped by business feature (cart, product, user, etc.).
This concept is related to Vertical Slice Architecture and Screaming Architecture (both referenced at the end), aiming for a state where "you can tell what the application does just by looking at the folder structure."
Problems with Technology-Based Organization
A common structure in React projects is this "technology-based" organization:
src/
├── components/ # UI components
│ ├── Button.tsx
│ ├── ProductCard.tsx
│ ├── CartItem.tsx
│ └── UserProfile.tsx
├── hooks/ # Custom hooks
│ ├── useCart.ts
│ ├── useProduct.ts
│ └── useUser.ts
├── utils/ # Utility functions
│ └── price.ts
└── App.tsx
The problem is that related code is scattered. When you want to "modify the cart feature," you need to search across components/, hooks/, and utils/. I found myself opening 6-7 files across different directories just to trace a single cart-related bug.
Stage 1 Structure (Feature-Based)
src/
├── features/
│ ├── cart/
│ │ ├── CartList.tsx
│ │ ├── useCart.ts
│ │ └── index.ts
│ ├── product/
│ │ ├── ProductCard.tsx
│ │ ├── useProduct.ts
│ │ └── index.ts
│ └── user/
│ ├── UserProfile.tsx
│ ├── useUser.ts
│ └── index.ts
├── components/ui/ # Shared UI (Button, etc.)
└── App.tsx
The index.ts files serve as barrel exports, defining the public interface of each feature and preventing direct imports of internal files.
Pros and Cons
| Pros | Cons |
|---|---|
| Simple to start | Hard to find files as they increase |
| Related code is nearby | No organization rules within features |
| Easy to add/remove features | Dependencies can become unclear |
Stage 2: Organize by Role Within Features
Problems with Stage 1
After developing with Stage 1 for a while, files within a single feature start to pile up.
src/features/cart/
├── CartList.tsx
├── CartItem.tsx
├── CartSummary.tsx
├── useCart.ts
├── useCartItem.ts
├── useDiscount.ts
├── calculateTotal.ts
├── cartTypes.ts
└── index.ts
At this point, finding "which are the components?" or "which are the hooks?" started taking time.
Organizing by Role
So I decided to organize by role within features. Putting UI in components, React hooks in hooks, and so on makes it quick to find what you're looking for.
Stage 2 Structure
src/features/
├── cart/
│ ├── components/ # UI components
│ │ ├── CartList.tsx
│ │ └── CartItem.tsx
│ ├── hooks/ # Custom hooks
│ │ ├── useCart.ts
│ │ └── useDiscount.ts
│ ├── utils/ # Utility functions
│ │ └── calculateTotal.ts
│ ├── types/ # TypeScript type definitions
│ │ └── index.ts
│ └── index.ts
├── product/
│ ├── components/
│ ├── hooks/
│ ├── utils/
│ ├── types/
│ └── index.ts
└── user/
└── ...
Pros and Cons
| Pros | Cons |
|---|---|
| Easy to find by file type | Folder depth increases |
| Roles become clear | Need to decide "where to put it" |
| Easy to standardize across team | Can't control logic dependencies |
At this stage, we're not yet thinking about "layers." This is just file classification.
Difference between Stage 2 and Stage 3: Up to Stage 2, the purpose is "file classification." From Stage 3, the purpose becomes "controlling dependency direction."
Stage 3: Introducing Clean Architecture Layers
Problems with Stage 2
As development continued with Stage 2, different problems emerged.
In my project, domain logic became complex, and I faced these challenges:
- Testing is difficult: Writing logic directly in hooks or components makes it dependent on React's rendering, harder to test
- Hard to reuse: Algorithms have nothing to do with React, but they're trapped inside hooks
- Dependencies are unclear: No rules about which file depends on which
The "separation of concerns" issues mentioned earlier became apparent as the project grew.
A Pragmatic Layer Structure (Inspired by Clean Architecture)
So I introduced a layer structure with conscious dependency direction, inspired by Clean Architecture.
⚠️ Important Note
Clean Architecture is originally a complete architecture meant to be applied to the entire project. You create domain/, useCases/, infrastructure/ layers directly under src/, and all features follow that layer structure.
What I'm introducing here is not that formal application. It's a custom approach of creating layers only within Features.
Why Create Layers Within Features?
Applying formal Clean Architecture to the entire project causes the same failure I mentioned at the beginning:
src/
├── domain/ # Domain logic for all features
│ ├── cart/
│ ├── product/
│ └── user/
├── useCases/ # Use cases for all features
│ ├── cart/
│ ├── product/
│ └── user/
├── infrastructure/ # API communication for all features
│ ├── cart/
│ ├── product/
│ └── user/
└── presentation/ # UI for all features
├── cart/
├── product/
└── user/
With this structure, "modifying the cart feature" requires jumping across 4 folders. This loses the benefit of Package by Feature (related code is nearby).
On the other hand, creating layers within Features means:
- Code stays organized by feature (maintaining Package by Feature benefits)
- Dependency direction becomes clear within that feature (borrowing Clean Architecture's concept)
- Layers can be introduced only to complex features (simple features stay at Stage 1-2)
In other words, I'm borrowing only the "controlling dependency direction" concept from Clean Architecture and applying it within the Package by Feature framework. This is my own interpretation and differs from formal Clean Architecture, so please be aware of that.
Stage 3 Structure
src/features/
├── cart/
│ ├── types/ # Type definitions only
│ │ └── index.ts
│ ├── domain/ # Business logic (pure functions)
│ │ └── priceCalculation.ts
│ ├── useCases/ # Use cases (operation units like "add to cart")
│ │ └── applyDiscount.ts
│ ├── infrastructure/ # External system integration (API, etc.)
│ │ └── cartRepository.ts
│ ├── hooks/ # React hooks
│ │ └── useCart.ts
│ ├── components/ # UI components
│ │ └── CartList.tsx
│ └── index.ts
└── ...
Terminology Notes
- domain: In this article, it roughly means "where business logic goes." It differs from the strict DDD definition.
- Difference between useCases and utils:
- utils: Generic functions. Usable regardless of business context (e.g.,
formatDate(),truncateString())- useCases: Business operation units. Can be expressed as a verb "to do something" (e.g.,
applyDiscount(),addToCart())- When in doubt, ask "can this function be used in other projects?" If yes, it's utils. If it's specific to this feature, it's useCases.
Role of Each Layer (My Personal Interpretation)
The following classification is what I arrived at through trial and error. It may differ from formal definitions, so please take it as a reference.
| Layer | Role | Dependencies |
|---|---|---|
| types | Type definitions only | None |
| domain | Business logic (pure functions) | types only |
| useCases | Use case implementation | domain, types |
| infrastructure | External system communication (API calls, etc.) | types |
| hooks | Bridge to React | useCases, infrastructure |
| components | UI | hooks |
In formal Clean Architecture, there's an "Interface Adapters" layer between layers to achieve dependency inversion. Also, hooks should access infrastructure through useCases, not directly. I've simplified for practicality. Please refer to the original sources if you want strict application.
Why connect hooks → infrastructure directly?
In frontend development, tools like TanStack Query (formerly React Query) are commonly used, and data fetching is tightly coupled with UI state (loading, error, cache, etc.). Making everything go through useCases can become verbose, so in practice, hooks often call infrastructure directly.
Dependency Direction
┌─────────────────────────────────────────────────────────┐
│ UI Layer │
│ ┌───────────────┐ │
│ │ components │ │
│ └───────┬───────┘ │
└──────────┼──────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Application Layer │
│ ┌───────────────┐ ┌───────────────┐ │
│ │ hooks │────────▶│ useCases │ │
│ └───────┬───────┘ └───────┬───────┘ │
└──────────┼─────────────────────────┼────────────────────┘
│ │
│ ▼
│ ┌──────────────────────────────────┐
│ │ Domain Layer │
│ │ ┌───────────────┐ │
│ │ │ domain │ ◀── No deps! │
│ │ └───────────────┘ │
│ └──────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────┐
│ Infrastructure Layer │
│ ┌───────────────┐ │
│ │infrastructure │ │
│ └───────────────┘ │
└─────────────────────────────────────────────────────────┘
The key point is domain doesn't depend on other layers. This keeps business logic independent from React and external APIs.
Pros and Cons
| Pros | Cons |
|---|---|
| Domain logic is easy to test | High learning cost |
| Clear dependencies make refactoring easier | Initial design takes time |
| Logic can be reused outside React | Excessive for small projects |
| Strong for long-term maintenance | Requires entire team's understanding |
Which Stage to Choose
"Stage 3 is the answer" is not the case. As mentioned at the beginning, the higher the Stage, the higher the complexity — there's a trade-off.
Cost of Complexity
| Stage | Maintainability | File Count | Understanding Cost | New Member Learning Curve |
|---|---|---|---|---|
| Stage 1 | Low-Medium | Few | Low | Gentle |
| Stage 2 | Medium | Moderate | Moderate | Moderate |
| Stage 3 | High | Many | High | Steep |
Abstraction is "an investment preparing for future changes," but if there's no return on that investment, you've just added complexity.
Address Pain When You Feel It
What I'd recommend is the approach of actually feeling the pain, then making the minimum change to solve that pain.
- YAGNI (You Ain't Gonna Need It): The principle of "don't build what you don't need now"
- Team buy-in: A team that has experienced a problem understands the need for the solution
- Avoiding over-engineering: Don't add complexity for imaginary problems
Rough Guidelines
The following is my personal criteria for judgment.
| Situation | Recommendation | Reason |
|---|---|---|
| Personal project / Prototype | Stage 1 | Priority is building something that works quickly |
| Small team / Speed-focused | Stage 1-2 | Everyone can grasp the whole codebase |
| Medium-sized / Multiple developers | Stage 2 | Need file organization rules |
| Complex domain logic | Stage 3 (partial) | Need to improve testability |
| Long-term operation / Maintainability-focused | Stage 2-3 | Investment to reduce future change costs |
| Many new members | Stage 1-2 | Keep learning costs down |
Migrate Gradually
You don't need to aim for Stage 3 from the start. When you feel pain, make the minimum change to solve it.
┌──────────────────────────┐
│ Technology-based struct │
└────────────┬─────────────┘
│ "Related code is scattered"
▼
┌──────────────────────────┐
│ Stage 1: Organize by │
│ feature │
└────────────┬─────────────┘
│ "Too many files in a feature"
▼
┌──────────────────────────┐
│ Stage 2: Organize by │
│ role │
└────────────┬─────────────┘
│ "Logic is complex, hard to test"
▼
┌──────────────────────────┐
│ Stage 3: Introduce │
│ layers │
└──────────────────────────┘
You don't need to migrate all features to Stage 3 at once. Just gradually migrate features with complex logic.
Having tests helps prevent regression during migration, but testing is outside the scope of this article.
Summary
There's no "single correct answer" for React folder structure.
In this article, I shared 3 gradual approaches that emerged from my personal trial and error.
- Stage 1: Package by Feature (organize by feature)
- Stage 2: Organize by role within features
- Stage 3: Introduce Clean Architecture layers
What matters is choosing a structure that fits your project's situation.
- Start with a flat structure, add abstraction as needed
- Be aware of abstraction costs (increased file count, understanding cost)
- Keep it within the range where the team understands "why this structure"
Over-engineering just adds complexity. When you feel pain, make the minimum structural change to solve it. I believe this cycle ultimately leads to a better development experience.
I hope this article helps someone struggling with folder structure decisions.
References
Original sources for the architecture patterns referenced in this article.
Package by Feature
The idea of organizing code by feature rather than technical layer.
Clean Architecture
Proposed by Robert C. Martin (Uncle Bob). Code is divided into concentric layers, with the rule that inner layers don't depend on outer layers.
Vertical Slice Architecture
Proposed by Jimmy Bogard. The idea of organizing code by feature (vertical) units.
Feature-Sliced Design
A systematized architecture methodology for frontend.
Screaming Architecture
The principle that "you should be able to tell what the application does just by looking at the folder structure."
Top comments (0)