DEV Community

usapopopooon
usapopopooon

Posted on

A Gradual Approach to React Folder Structure: From Package by Feature to Clean Architecture

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>
)
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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│
                   └──────────┘      └──────────────┘
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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/
    └── ...
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

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
   └── ...
Enter fullscreen mode Exit fullscreen mode

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 │                                      │
│  └───────────────┘                                      │
└─────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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          │
└──────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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.

  1. Stage 1: Package by Feature (organize by feature)
  2. Stage 2: Organize by role within features
  3. 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)