React-Architecture-Approaches

React Architecture Approaches: A Comprehensive Guide

React TypeScript Architecture

πŸ“– Overview

Choosing the right architecture for your React application is crucial for long-term maintainability, scalability, and team productivity. This guide explores four popular architectural approaches, their philosophies, implementation details, and ideal use cases.

What You’ll Learn


Table of Contents

  1. Atomic Design Approach
  2. Domain-Driven Design (DDD) Approach
  3. Feature-Sliced Design (FSD) Approach
  4. Hybrid Approach
  5. Comparison Table
  6. Which Approach Should You Choose?
  7. Recommendations

1. Atomic Design Approach

🎯 Philosophy

β€œBuild from the smallest to the largest”

Atomic Design breaks down UI components into five distinct levels based on their complexity and composition:

This methodology, inspired by chemistry, creates a systematic approach to building consistent, reusable UI components.

πŸ“‚ Complete Structure

project/
β”œβ”€β”€ public/
β”‚   β”œβ”€β”€ index.html
β”‚   └── favicon.ico
β”‚
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”œβ”€β”€ atoms/                      # Smallest, indivisible components
β”‚   β”‚   β”‚   β”œβ”€β”€ Button/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ Button.tsx
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ Button.module.css
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ Button.test.tsx
β”‚   β”‚   β”‚   β”‚   └── Button.stories.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ Input/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ Input.tsx
β”‚   β”‚   β”‚   β”‚   └── Input.module.css
β”‚   β”‚   β”‚   β”œβ”€β”€ Label/
β”‚   β”‚   β”‚   β”‚   └── Label.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ Icon/
β”‚   β”‚   β”‚   β”‚   └── Icon.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ Avatar/
β”‚   β”‚   β”‚   β”‚   └── Avatar.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ Badge/
β”‚   β”‚   β”‚   β”‚   └── Badge.tsx
β”‚   β”‚   β”‚   └── index.ts               # Barrel exports
β”‚   β”‚   β”‚
β”‚   β”‚   β”œβ”€β”€ molecules/                  # Combinations of atoms
β”‚   β”‚   β”‚   β”œβ”€β”€ SearchBar/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ SearchBar.tsx      # Input + Button + Icon
β”‚   β”‚   β”‚   β”‚   └── SearchBar.module.css
β”‚   β”‚   β”‚   β”œβ”€β”€ UserCard/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ UserCard.tsx       # Avatar + Text + Badge
β”‚   β”‚   β”‚   β”‚   └── UserCard.module.css
β”‚   β”‚   β”‚   β”œβ”€β”€ FormField/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ FormField.tsx      # Label + Input + Error
β”‚   β”‚   β”‚   β”‚   └── FormField.module.css
β”‚   β”‚   β”‚   β”œβ”€β”€ NavigationItem/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ NavigationItem.tsx # Icon + Link + Badge
β”‚   β”‚   β”‚   β”‚   └── NavigationItem.module.css
β”‚   β”‚   β”‚   β”œβ”€β”€ ProductCard/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ ProductCard.tsx    # Image + Title + Price + Button
β”‚   β”‚   β”‚   β”‚   └── ProductCard.module.css
β”‚   β”‚   β”‚   └── index.ts
β”‚   β”‚   β”‚
β”‚   β”‚   β”œβ”€β”€ organisms/                  # Complex UI sections
β”‚   β”‚   β”‚   β”œβ”€β”€ Header/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ Header.tsx        # Logo + Navigation + SearchBar
β”‚   β”‚   β”‚   β”‚   └── Header.module.css
β”‚   β”‚   β”‚   β”œβ”€β”€ Footer/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ Footer.tsx
β”‚   β”‚   β”‚   β”‚   └── Footer.module.css
β”‚   β”‚   β”‚   β”œβ”€β”€ ProductList/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ ProductList.tsx   # Multiple ProductCards + Filter
β”‚   β”‚   β”‚   β”‚   └── ProductList.module.css
β”‚   β”‚   β”‚   β”œβ”€β”€ Sidebar/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ Sidebar.tsx       # NavigationItems + UserCard
β”‚   β”‚   β”‚   β”‚   └── Sidebar.module.css
β”‚   β”‚   β”‚   β”œβ”€β”€ DashboardWidget/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ DashboardWidget.tsx
β”‚   β”‚   β”‚   β”‚   └── DashboardWidget.module.css
β”‚   β”‚   β”‚   └── index.ts
β”‚   β”‚   β”‚
β”‚   β”‚   β”œβ”€β”€ templates/                  # Page layouts with placeholders
β”‚   β”‚   β”‚   β”œβ”€β”€ DefaultLayout/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ DefaultLayout.tsx # Header + Main + Footer
β”‚   β”‚   β”‚   β”‚   └── DefaultLayout.module.css
β”‚   β”‚   β”‚   β”œβ”€β”€ DashboardLayout/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ DashboardLayout.tsx # Sidebar + Header + Content
β”‚   β”‚   β”‚   β”‚   └── DashboardLayout.module.css
β”‚   β”‚   β”‚   β”œβ”€β”€ AuthLayout/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ AuthLayout.tsx    # Only form container
β”‚   β”‚   β”‚   β”‚   └── AuthLayout.module.css
β”‚   β”‚   β”‚   └── index.ts
β”‚   β”‚   β”‚
β”‚   β”‚   └── pages/                      # Complete pages (templates filled)
β”‚   β”‚       β”œβ”€β”€ HomePage.tsx
β”‚   β”‚       β”œβ”€β”€ DashboardPage.tsx
β”‚   β”‚       β”œβ”€β”€ ProductsPage.tsx
β”‚   β”‚       β”œβ”€β”€ LoginPage.tsx
β”‚   β”‚       └── UserProfilePage.tsx
β”‚   β”‚
β”‚   β”œβ”€β”€ hooks/
β”‚   β”‚   β”œβ”€β”€ useAuth.ts
β”‚   β”‚   β”œβ”€β”€ useFetch.ts
β”‚   β”‚   └── useLocalStorage.ts
β”‚   β”‚
β”‚   β”œβ”€β”€ services/
β”‚   β”‚   β”œβ”€β”€ apiClient.ts
β”‚   β”‚   β”œβ”€β”€ authService.ts
β”‚   β”‚   └── productService.ts
β”‚   β”‚
β”‚   β”œβ”€β”€ store/
β”‚   β”‚   β”œβ”€β”€ slices/
β”‚   β”‚   β”‚   β”œβ”€β”€ authSlice.ts
β”‚   β”‚   β”‚   └── productSlice.ts
β”‚   β”‚   └── store.ts
β”‚   β”‚
β”‚   β”œβ”€β”€ styles/
β”‚   β”‚   β”œβ”€β”€ global.scss
β”‚   β”‚   └── variables.scss
β”‚   β”‚
β”‚   β”œβ”€β”€ types/
β”‚   β”‚   β”œβ”€β”€ components.types.ts
β”‚   β”‚   └── api.types.ts
β”‚   β”‚
β”‚   β”œβ”€β”€ utils/
β”‚   β”‚   β”œβ”€β”€ formatters.ts
β”‚   β”‚   └── validators.ts
β”‚   β”‚
β”‚   β”œβ”€β”€ App.tsx
β”‚   β”œβ”€β”€ index.tsx
β”‚   └── router.tsx
β”‚
β”œβ”€β”€ package.json
└── tsconfig.json

πŸ’» Example Code

Atom: Button Component

// components/atoms/Button/Button.tsx
interface ButtonProps {
  variant: 'primary' | 'secondary' | 'danger';
  size: 'sm' | 'md' | 'lg';
  children: React.ReactNode;
  onClick?: () => void;
  disabled?: boolean;
  loading?: boolean;
}

export const Button: React.FC<ButtonProps> = ({
  variant,
  size,
  children,
  onClick,
  disabled,
  loading,
}) => {
  return (
    <button
      className={`btn btn-${variant} btn-${size}`}
      onClick={onClick}
      disabled={disabled || loading}
      aria-busy={loading}
    >
      {loading ? <Spinner /> : children}
    </button>
  );
};

Molecule: SearchBar Component

// components/molecules/SearchBar/SearchBar.tsx
import { Input, Button, Icon } from '../../atoms';

interface SearchBarProps {
  onSearch: (query: string) => void;
  placeholder?: string;
  initialValue?: string;
}

export const SearchBar: React.FC<SearchBarProps> = ({
  onSearch,
  placeholder = 'Search...',
  initialValue = '',
}) => {
  const [query, setQuery] = useState(initialValue);

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (query.trim()) {
      onSearch(query);
    }
  };

  return (
    <form className="search-bar" onSubmit={handleSubmit} role="search">
      <Icon name="search" aria-hidden="true" />
      <Input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder={placeholder}
        aria-label="Search"
      />
      <Button variant="primary" size="md" type="submit">
        Search
      </Button>
    </form>
  );
};

Organism: Header Component

// components/organisms/Header/Header.tsx
import { Logo, Navigation, SearchBar, UserMenu } from '../../molecules';

interface HeaderProps {
  user?: User;
  navItems: NavItem[];
}

export const Header: React.FC<HeaderProps> = ({ user, navItems }) => {
  return (
    <header className="header" role="banner">
      <div className="header__container">
        <Logo />
        <Navigation items={navItems} />
        <SearchBar onSearch={handleSearch} />
        <UserMenu user={user} />
      </div>
    </header>
  );
};

βœ… Pros

Advantage Description
High Reusability Components are built to be reused across the application
Consistent UI Standardized naming and structure ensure design consistency
Easy Testing Small, focused components are straightforward to test
Design System Friendly Perfect foundation for building and maintaining UI libraries
Clear Hierarchy Easy to understand component relationships

❌ Cons

Disadvantage Description
Over-engineering Can be too granular for simple applications
Feature Scattering Related logic is spread across different folders
Complex Navigation Can be difficult to locate components in large projects
Rigid Structure May not accommodate complex business logic well

🎯 Best For


2. Domain-Driven Design (DDD) Approach

🎯 Philosophy

β€œOrganize around business domains, not technical layers”

Domain-Driven Design organizes code around business domains (bounded contexts) rather than technical concerns. Each domain (e.g., Users, Products, Orders) contains everything it needs: components, hooks, services, types, and state management.

πŸ“‚ Complete Structure

src/
β”œβ”€β”€ domains/                           # Business domains (bounded contexts)
β”‚   β”‚
β”‚   β”œβ”€β”€ auth/                          # Authentication domain
β”‚   β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”‚   β”œβ”€β”€ LoginForm.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ RegisterForm.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ ForgotPassword.tsx
β”‚   β”‚   β”‚   └── AuthGuard.tsx
β”‚   β”‚   β”œβ”€β”€ hooks/
β”‚   β”‚   β”‚   β”œβ”€β”€ useAuth.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ useLogin.ts
β”‚   β”‚   β”‚   └── useSession.ts
β”‚   β”‚   β”œβ”€β”€ services/
β”‚   β”‚   β”‚   β”œβ”€β”€ auth.service.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ token.service.ts
β”‚   β”‚   β”‚   └── session.service.ts
β”‚   β”‚   β”œβ”€β”€ store/
β”‚   β”‚   β”‚   β”œβ”€β”€ auth.slice.ts
β”‚   β”‚   β”‚   └── auth.selectors.ts
β”‚   β”‚   β”œβ”€β”€ types/
β”‚   β”‚   β”‚   β”œβ”€β”€ auth.types.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ user.types.ts
β”‚   β”‚   β”‚   └── session.types.ts
β”‚   β”‚   β”œβ”€β”€ utils/
β”‚   β”‚   β”‚   β”œβ”€β”€ auth.validators.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ password.utils.ts
β”‚   β”‚   β”‚   └── token.utils.ts
β”‚   β”‚   β”œβ”€β”€ pages/
β”‚   β”‚   β”‚   β”œβ”€β”€ LoginPage.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ RegisterPage.tsx
β”‚   β”‚   β”‚   └── ResetPasswordPage.tsx
β”‚   β”‚   β”œβ”€β”€ constants/
β”‚   β”‚   β”‚   β”œβ”€β”€ auth.constants.ts
β”‚   β”‚   β”‚   └── roles.constants.ts
β”‚   β”‚   β”œβ”€β”€ auth.test.ts
β”‚   β”‚   └── index.ts                  # Public API
β”‚   β”‚
β”‚   β”œβ”€β”€ users/                         # Users domain
β”‚   β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”‚   β”œβ”€β”€ UserProfile.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ UserList.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ UserCard.tsx
β”‚   β”‚   β”‚   └── UserSettings.tsx
β”‚   β”‚   β”œβ”€β”€ hooks/
β”‚   β”‚   β”‚   β”œβ”€β”€ useUsers.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ useUserProfile.ts
β”‚   β”‚   β”‚   └── useUserRoles.ts
β”‚   β”‚   β”œβ”€β”€ services/
β”‚   β”‚   β”‚   β”œβ”€β”€ user.service.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ user.repository.ts
β”‚   β”‚   β”‚   └── user.api.ts
β”‚   β”‚   β”œβ”€β”€ store/
β”‚   β”‚   β”‚   β”œβ”€β”€ user.slice.ts
β”‚   β”‚   β”‚   └── user.selectors.ts
β”‚   β”‚   β”œβ”€β”€ types/
β”‚   β”‚   β”‚   β”œβ”€β”€ user.types.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ role.types.ts
β”‚   β”‚   β”‚   └── permission.types.ts
β”‚   β”‚   β”œβ”€β”€ utils/
β”‚   β”‚   β”‚   β”œβ”€β”€ user.validators.ts
β”‚   β”‚   β”‚   └── user.formatters.ts
β”‚   β”‚   β”œβ”€β”€ pages/
β”‚   β”‚   β”‚   β”œβ”€β”€ UsersPage.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ UserDetailsPage.tsx
β”‚   β”‚   β”‚   └── UserEditPage.tsx
β”‚   β”‚   β”œβ”€β”€ user.test.ts
β”‚   β”‚   └── index.ts
β”‚   β”‚
β”‚   β”œβ”€β”€ products/                      # Products domain
β”‚   β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”‚   β”œβ”€β”€ ProductCard.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ ProductList.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ ProductForm.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ ProductFilters.tsx
β”‚   β”‚   β”‚   └── ProductReview.tsx
β”‚   β”‚   β”œβ”€β”€ hooks/
β”‚   β”‚   β”‚   β”œβ”€β”€ useProducts.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ useProductSearch.ts
β”‚   β”‚   β”‚   └── useProductFilters.ts
β”‚   β”‚   β”œβ”€β”€ services/
β”‚   β”‚   β”‚   β”œβ”€β”€ product.service.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ product.cache.ts
β”‚   β”‚   β”‚   └── product.api.ts
β”‚   β”‚   β”œβ”€β”€ store/
β”‚   β”‚   β”‚   β”œβ”€β”€ product.slice.ts
β”‚   β”‚   β”‚   └── product.selectors.ts
β”‚   β”‚   β”œβ”€β”€ types/
β”‚   β”‚   β”‚   β”œβ”€β”€ product.types.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ category.types.ts
β”‚   β”‚   β”‚   └── inventory.types.ts
β”‚   β”‚   β”œβ”€β”€ utils/
β”‚   β”‚   β”‚   β”œβ”€β”€ product.validators.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ product.formatters.ts
β”‚   β”‚   β”‚   └── product.calculators.ts
β”‚   β”‚   β”œβ”€β”€ pages/
β”‚   β”‚   β”‚   β”œβ”€β”€ ProductsPage.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ ProductDetailsPage.tsx
β”‚   β”‚   β”‚   └── ProductCreatePage.tsx
β”‚   β”‚   β”œβ”€β”€ product.test.ts
β”‚   β”‚   └── index.ts
β”‚   β”‚
β”‚   β”œβ”€β”€ orders/                        # Orders domain
β”‚   β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”œβ”€β”€ hooks/
β”‚   β”‚   β”œβ”€β”€ services/
β”‚   β”‚   β”œβ”€β”€ store/
β”‚   β”‚   β”œβ”€β”€ types/
β”‚   β”‚   β”œβ”€β”€ utils/
β”‚   β”‚   β”œβ”€β”€ pages/
β”‚   β”‚   └── index.ts
β”‚   β”‚
β”‚   └── shared/                        # Shared across domains
β”‚       β”œβ”€β”€ components/
β”‚       β”‚   β”œβ”€β”€ Button.tsx
β”‚       β”‚   β”œβ”€β”€ Modal.tsx
β”‚       β”‚   β”œβ”€β”€ Spinner.tsx
β”‚       β”‚   └── Toast.tsx
β”‚       β”œβ”€β”€ hooks/
β”‚       β”‚   β”œβ”€β”€ useDebounce.ts
β”‚       β”‚   └── useLocalStorage.ts
β”‚       β”œβ”€β”€ types/
β”‚       β”‚   β”œβ”€β”€ common.types.ts
β”‚       β”‚   └── api.types.ts
β”‚       β”œβ”€β”€ utils/
β”‚       β”‚   β”œβ”€β”€ formatters.ts
β”‚       β”‚   β”œβ”€β”€ validators.ts
β”‚       β”‚   └── date.utils.ts
β”‚       β”œβ”€β”€ constants/
β”‚       β”‚   β”œβ”€β”€ routes.ts
β”‚       β”‚   └── app.constants.ts
β”‚       └── services/
β”‚           └── api.service.ts
β”‚
β”œβ”€β”€ app/                               # App-level configuration
β”‚   β”œβ”€β”€ config/
β”‚   β”‚   β”œβ”€β”€ env.ts
β”‚   β”‚   └── app.config.ts
β”‚   β”œβ”€β”€ store/
β”‚   β”‚   └── store.ts                  # Root store combining domain stores
β”‚   β”œβ”€β”€ router/
β”‚   β”‚   └── router.tsx
β”‚   β”œβ”€β”€ styles/
β”‚   β”‚   └── global.scss
β”‚   β”œβ”€β”€ App.tsx
β”‚   └── index.tsx
β”‚
└── infrastructure/                    # External integrations
    β”œβ”€β”€ api/
    β”œβ”€β”€ logging/
    β”œβ”€β”€ monitoring/
    └── analytics/

πŸ’» Example Code

Domain Types

// domains/auth/types/auth.types.ts
export interface User {
  id: string;
  email: string;
  firstName: string;
  lastName: string;
  roles: UserRole[];
  permissions: Permission[];
  createdAt: Date;
  updatedAt: Date;
}

export enum UserRole {
  ADMIN = 'admin',
  USER = 'user',
  MODERATOR = 'moderator',
}

export enum Permission {
  READ_USERS = 'read:users',
  WRITE_USERS = 'write:users',
  DELETE_USERS = 'delete:users',
  READ_PRODUCTS = 'read:products',
  WRITE_PRODUCTS = 'write:products',
}

export interface AuthState {
  user: User | null;
  isAuthenticated: boolean;
  isLoading: boolean;
  error: string | null;
  token: string | null;
}

Domain Service

// domains/auth/services/auth.service.ts
import { User, AuthState } from '../types/auth.types';
import { apiClient } from '../../../infrastructure/api';

export class AuthService {
  static async login(email: string, password: string): Promise<User> {
    try {
      const response = await apiClient.post('/auth/login', { email, password });
      const { user, token } = response.data;
      
      // Store token
      localStorage.setItem('access_token', token);
      apiClient.defaults.headers.common['Authorization'] = `Bearer ${token}`;
      
      return user;
    } catch (error) {
      throw new Error('Authentication failed');
    }
  }

  static async logout(): Promise<void> {
    try {
      await apiClient.post('/auth/logout');
    } finally {
      localStorage.removeItem('access_token');
      delete apiClient.defaults.headers.common['Authorization'];
    }
  }

  static async refreshToken(): Promise<string> {
    const response = await apiClient.post('/auth/refresh');
    const { token } = response.data;
    localStorage.setItem('access_token', token);
    return token;
  }

  static async getCurrentUser(): Promise<User> {
    const response = await apiClient.get('/auth/me');
    return response.data;
  }
}

Domain Hook

// domains/auth/hooks/useAuth.ts
import { useAuthStore } from '../store/auth.slice';
import { AuthService } from '../services/auth.service';
import { useCallback } from 'react';

export const useAuth = () => {
  const { user, isAuthenticated, isLoading, error } = useAuthStore();

  const login = useCallback(async (email: string, password: string) => {
    try {
      useAuthStore.getState().setLoading(true);
      const user = await AuthService.login(email, password);
      useAuthStore.getState().setUser(user);
    } catch (error) {
      useAuthStore.getState().setError(error.message);
    } finally {
      useAuthStore.getState().setLoading(false);
    }
  }, []);

  const logout = useCallback(async () => {
    try {
      await AuthService.logout();
      useAuthStore.getState().clearUser();
    } catch (error) {
      console.error('Logout failed:', error);
    }
  }, []);

  const refreshSession = useCallback(async () => {
    try {
      const token = await AuthService.refreshToken();
      const user = await AuthService.getCurrentUser();
      useAuthStore.getState().setUser(user);
      return token;
    } catch (error) {
      useAuthStore.getState().clearUser();
      throw error;
    }
  }, []);

  return {
    user,
    isAuthenticated,
    isLoading,
    error,
    login,
    logout,
    refreshSession,
  };
};

Domain Component

// domains/auth/components/LoginForm.tsx
import { useAuth } from '../hooks/useAuth';
import { Button, Input } from '../../shared/components';
import { useForm } from '../../shared/hooks/useForm';
import { authValidators } from '../utils/auth.validators';

interface LoginFormData {
  email: string;
  password: string;
}

export const LoginForm: React.FC = () => {
  const { login, isLoading } = useAuth();
  const { values, errors, handleChange, handleSubmit } = useForm<LoginFormData>({
    initialValues: { email: '', password: '' },
    validate: authValidators.login,
    onSubmit: (data) => login(data.email, data.password),
  });

  return (
    <form onSubmit={handleSubmit} className="login-form" noValidate>
      <div className="login-form__header">
        <h2>Welcome Back</h2>
        <p>Sign in to your account to continue</p>
      </div>

      <Input
        type="email"
        name="email"
        value={values.email}
        onChange={handleChange}
        label="Email Address"
        placeholder="you@example.com"
        required
        error={errors.email}
        disabled={isLoading}
        autoFocus
      />

      <Input
        type="password"
        name="password"
        value={values.password}
        onChange={handleChange}
        label="Password"
        placeholder="Enter your password"
        required
        error={errors.password}
        disabled={isLoading}
      />

      <div className="login-form__actions">
        <Button
          type="submit"
          variant="primary"
          size="lg"
          loading={isLoading}
          disabled={isLoading}
          fullWidth
        >
          {isLoading ? 'Signing in...' : 'Sign In'}
        </Button>
      </div>

      <div className="login-form__footer">
        <a href="/forgot-password">Forgot password?</a>
        <span>|</span>
        <a href="/register">Create account</a>
      </div>
    </form>
  );
};

βœ… Pros

Advantage Description
Business Logic Centric Easy to understand and modify business rules
Autonomous Teams Teams can work on separate domains independently
High Cohesion Related code lives together, reducing cognitive load
Scalable Easy to add new domains without affecting others
Maintainability Clear boundaries make code easier to maintain

❌ Cons

Disadvantage Description
Initial Complexity More setup and planning required
Domain Boundaries Can be blurry between domains
Shared Components Need careful management of shared code
Overhead May be overkill for simple applications

🎯 Best For


3. Feature-Sliced Design (FSD) Approach

🎯 Philosophy

β€œStrict layers with clear dependency rules”

Feature-Sliced Design is a structural architecture that organizes code into slices (features) with strict layering:

App β†’ Pages β†’ Widgets β†’ Features β†’ Entities β†’ Shared

Each layer can only depend on layers below it, creating a clear, predictable architecture that scales well.

πŸ“‚ Complete Structure

src/
β”œβ”€β”€ app/                               # App-level configurations
β”‚   β”œβ”€β”€ config/
β”‚   β”‚   β”œβ”€β”€ env.ts
β”‚   β”‚   └── app.config.ts
β”‚   β”œβ”€β”€ providers/
β”‚   β”‚   β”œβ”€β”€ ThemeProvider.tsx
β”‚   β”‚   β”œβ”€β”€ StoreProvider.tsx
β”‚   β”‚   └── RouterProvider.tsx
β”‚   β”œβ”€β”€ styles/
β”‚   β”‚   β”œβ”€β”€ _reset.scss
β”‚   β”‚   β”œβ”€β”€ _variables.scss
β”‚   β”‚   └── global.scss
β”‚   β”œβ”€β”€ App.tsx
β”‚   β”œβ”€β”€ index.tsx
β”‚   └── types.ts
β”‚
β”œβ”€β”€ pages/                             # Page-level composition
β”‚   β”œβ”€β”€ auth-page/
β”‚   β”‚   β”œβ”€β”€ ui/
β”‚   β”‚   β”‚   β”œβ”€β”€ AuthPage.tsx
β”‚   β”‚   β”‚   └── AuthPage.module.scss
β”‚   β”‚   └── index.ts
β”‚   β”œβ”€β”€ dashboard-page/
β”‚   β”‚   β”œβ”€β”€ ui/
β”‚   β”‚   β”‚   β”œβ”€β”€ DashboardPage.tsx
β”‚   β”‚   β”‚   └── DashboardPage.module.scss
β”‚   β”‚   └── index.ts
β”‚   β”œβ”€β”€ products-page/
β”‚   β”‚   β”œβ”€β”€ ui/
β”‚   β”‚   β”‚   β”œβ”€β”€ ProductsPage.tsx
β”‚   β”‚   β”‚   └── ProductsPage.module.scss
β”‚   β”‚   └── index.ts
β”‚   └── index.ts
β”‚
β”œβ”€β”€ widgets/                           # Reusable blocks (larger than features)
β”‚   β”œβ”€β”€ header/
β”‚   β”‚   β”œβ”€β”€ ui/
β”‚   β”‚   β”‚   β”œβ”€β”€ Header.tsx
β”‚   β”‚   β”‚   └── Header.module.scss
β”‚   β”‚   β”œβ”€β”€ model/
β”‚   β”‚   β”‚   β”œβ”€β”€ header.model.ts
β”‚   β”‚   β”‚   └── header.types.ts
β”‚   β”‚   └── index.ts
β”‚   β”œβ”€β”€ sidebar/
β”‚   β”‚   β”œβ”€β”€ ui/
β”‚   β”‚   β”œβ”€β”€ model/
β”‚   β”‚   └── index.ts
β”‚   β”œβ”€β”€ product-list/
β”‚   β”‚   β”œβ”€β”€ ui/
β”‚   β”‚   β”‚   β”œβ”€β”€ ProductList.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ ProductCard.tsx
β”‚   β”‚   β”‚   └── ProductList.module.scss
β”‚   β”‚   β”œβ”€β”€ model/
β”‚   β”‚   β”‚   β”œβ”€β”€ productList.model.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ productList.types.ts
β”‚   β”‚   β”‚   └── productList.selectors.ts
β”‚   β”‚   β”œβ”€β”€ api/
β”‚   β”‚   β”‚   └── productList.api.ts
β”‚   β”‚   └── index.ts
β”‚   └── index.ts
β”‚
β”œβ”€β”€ features/                          # User interactions/scenarios
β”‚   β”œβ”€β”€ auth/
β”‚   β”‚   β”œβ”€β”€ ui/
β”‚   β”‚   β”‚   β”œβ”€β”€ LoginForm.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ RegisterForm.tsx
β”‚   β”‚   β”‚   └── AuthForm.module.scss
β”‚   β”‚   β”œβ”€β”€ model/
β”‚   β”‚   β”‚   β”œβ”€β”€ auth.model.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ auth.types.ts
β”‚   β”‚   β”‚   └── auth.selectors.ts
β”‚   β”‚   β”œβ”€β”€ api/
β”‚   β”‚   β”‚   β”œβ”€β”€ auth.api.ts
β”‚   β”‚   β”‚   └── auth.endpoints.ts
β”‚   β”‚   β”œβ”€β”€ lib/
β”‚   β”‚   β”‚   β”œβ”€β”€ auth.mappers.ts
β”‚   β”‚   β”‚   └── auth.validators.ts
β”‚   β”‚   β”œβ”€β”€ hooks/
β”‚   β”‚   β”‚   β”œβ”€β”€ useAuth.ts
β”‚   β”‚   β”‚   └── useLogin.ts
β”‚   β”‚   └── index.ts
β”‚   β”‚
β”‚   β”œβ”€β”€ product-search/
β”‚   β”‚   β”œβ”€β”€ ui/
β”‚   β”‚   β”‚   β”œβ”€β”€ SearchBar.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ SearchFilters.tsx
β”‚   β”‚   β”‚   └── SearchResults.tsx
β”‚   β”‚   β”œβ”€β”€ model/
β”‚   β”‚   β”‚   β”œβ”€β”€ search.model.ts
β”‚   β”‚   β”‚   └── search.types.ts
β”‚   β”‚   β”œβ”€β”€ api/
β”‚   β”‚   β”‚   └── search.api.ts
β”‚   β”‚   β”œβ”€β”€ hooks/
β”‚   β”‚   β”‚   β”œβ”€β”€ useSearch.ts
β”‚   β”‚   β”‚   └── useFilters.ts
β”‚   β”‚   └── index.ts
β”‚   β”‚
β”‚   β”œβ”€β”€ user-profile/
β”‚   β”‚   β”œβ”€β”€ ui/
β”‚   β”‚   β”œβ”€β”€ model/
β”‚   β”‚   β”œβ”€β”€ api/
β”‚   β”‚   β”œβ”€β”€ hooks/
β”‚   β”‚   └── index.ts
β”‚   β”‚
β”‚   └── index.ts
β”‚
β”œβ”€β”€ entities/                          # Business entities
β”‚   β”œβ”€β”€ user/
β”‚   β”‚   β”œβ”€β”€ ui/
β”‚   β”‚   β”‚   β”œβ”€β”€ UserCard.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ UserAvatar.tsx
β”‚   β”‚   β”‚   └── UserCard.module.scss
β”‚   β”‚   β”œβ”€β”€ model/
β”‚   β”‚   β”‚   β”œβ”€β”€ user.model.ts
β”‚   β”‚   β”‚   └── user.types.ts
β”‚   β”‚   β”œβ”€β”€ api/
β”‚   β”‚   β”‚   └── user.api.ts
β”‚   β”‚   β”œβ”€β”€ lib/
β”‚   β”‚   β”‚   └── user.validators.ts
β”‚   β”‚   └── index.ts
β”‚   β”‚
β”‚   β”œβ”€β”€ product/
β”‚   β”‚   β”œβ”€β”€ ui/
β”‚   β”‚   β”‚   β”œβ”€β”€ ProductCard.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ ProductPrice.tsx
β”‚   β”‚   β”‚   └── ProductCard.module.scss
β”‚   β”‚   β”œβ”€β”€ model/
β”‚   β”‚   β”‚   β”œβ”€β”€ product.model.ts
β”‚   β”‚   β”‚   └── product.types.ts
β”‚   β”‚   β”œβ”€β”€ api/
β”‚   β”‚   β”‚   └── product.api.ts
β”‚   β”‚   β”œβ”€β”€ lib/
β”‚   β”‚   β”‚   β”œβ”€β”€ product.validators.ts
β”‚   β”‚   β”‚   └── product.formatters.ts
β”‚   β”‚   └── index.ts
β”‚   β”‚
β”‚   β”œβ”€β”€ order/
β”‚   β”‚   β”œβ”€β”€ ui/
β”‚   β”‚   β”œβ”€β”€ model/
β”‚   β”‚   β”œβ”€β”€ api/
β”‚   β”‚   └── index.ts
β”‚   β”‚
β”‚   └── index.ts
β”‚
└── shared/                           # Shared code (lowest layer)
    β”œβ”€β”€ ui/
    β”‚   β”œβ”€β”€ atoms/
    β”‚   β”‚   β”œβ”€β”€ Button/
    β”‚   β”‚   β”œβ”€β”€ Input/
    β”‚   β”‚   β”œβ”€β”€ Label/
    β”‚   β”‚   └── Icon/
    β”‚   β”œβ”€β”€ molecules/
    β”‚   β”‚   β”œβ”€β”€ FormField/
    β”‚   β”‚   └── Toast/
    β”‚   β”œβ”€β”€ organisms/
    β”‚   β”‚   └── Modal/
    β”‚   └── index.ts
    β”‚
    β”œβ”€β”€ api/
    β”‚   β”œβ”€β”€ apiClient.ts
    β”‚   β”œβ”€β”€ endpoints.ts
    β”‚   └── interceptors.ts
    β”‚
    β”œβ”€β”€ lib/
    β”‚   β”œβ”€β”€ react-query.ts
    β”‚   β”œβ”€β”€ i18n.ts
    β”‚   └── sentry.ts
    β”‚
    β”œβ”€β”€ hooks/
    β”‚   β”œβ”€β”€ useDebounce.ts
    β”‚   β”œβ”€β”€ useLocalStorage.ts
    β”‚   └── useWindowSize.ts
    β”‚
    β”œβ”€β”€ utils/
    β”‚   β”œβ”€β”€ formatters/
    β”‚   β”œβ”€β”€ validators/
    β”‚   └── helpers/
    β”‚
    β”œβ”€β”€ types/
    β”‚   β”œβ”€β”€ common.types.ts
    β”‚   └── api.types.ts
    β”‚
    β”œβ”€β”€ constants/
    β”‚   β”œβ”€β”€ routes.ts
    β”‚   └── app.constants.ts
    β”‚
    └── config/
        └── env.ts

πŸ’» Example Code

Shared Components

// shared/ui/atoms/Button/Button.tsx
export interface ButtonProps {
  variant: 'primary' | 'secondary' | 'danger' | 'ghost';
  size: 'sm' | 'md' | 'lg';
  children: React.ReactNode;
  onClick?: () => void;
  loading?: boolean;
  disabled?: boolean;
  fullWidth?: boolean;
  type?: 'button' | 'submit' | 'reset';
}

export const Button: React.FC<ButtonProps> = ({
  variant = 'primary',
  size = 'md',
  children,
  onClick,
  loading = false,
  disabled = false,
  fullWidth = false,
  type = 'button',
}) => {
  const className = classNames(
    'btn',
    `btn--${variant}`,
    `btn--${size}`,
    {
      'btn--loading': loading,
      'btn--full-width': fullWidth,
    }
  );

  return (
    <button
      className={className}
      onClick={onClick}
      disabled={disabled || loading}
      type={type}
      aria-busy={loading}
    >
      {loading && <Spinner size="sm" className="btn__spinner" />}
      <span className="btn__content">{children}</span>
    </button>
  );
};

Entity Model

// entities/product/model/product.types.ts
export interface Product {
  id: string;
  name: string;
  description: string;
  price: number;
  category: ProductCategory;
  stock: number;
  images: string[];
  rating: number;
  reviews: ProductReview[];
  createdAt: Date;
  updatedAt: Date;
}

export interface ProductCategory {
  id: string;
  name: string;
  slug: string;
  description?: string;
}

export interface ProductReview {
  id: string;
  userId: string;
  rating: number;
  comment: string;
  createdAt: Date;
}

// entities/product/model/product.model.ts
import { Product } from './product.types';

export const productModel = {
  calculateDiscountPrice: (product: Product, discount: number): number => {
    return Math.round(product.price * (1 - discount / 100) * 100) / 100;
  },

  isInStock: (product: Product): boolean => {
    return product.stock > 0;
  },

  getStockStatus: (product: Product): 'in-stock' | 'low-stock' | 'out-of-stock' => {
    if (product.stock === 0) return 'out-of-stock';
    if (product.stock < 10) return 'low-stock';
    return 'in-stock';
  },

  getAverageRating: (product: Product): number => {
    if (product.reviews.length === 0) return 0;
    const sum = product.reviews.reduce((acc, review) => acc + review.rating, 0);
    return Math.round((sum / product.reviews.length) * 10) / 10;
  },

  getProductImageUrl: (product: Product, index: number = 0): string => {
    return product.images[index] || '/images/product-placeholder.jpg';
  },

  formatPrice: (product: Product): string => {
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'USD',
    }).format(product.price);
  },
};

Feature Model

// features/product-search/model/search.types.ts
import { Product } from '../../../entities/product/model/product.types';

export interface SearchState {
  query: string;
  filters: SearchFilters;
  results: Product[];
  isLoading: boolean;
  error: string | null;
  hasMore: boolean;
  page: number;
}

export interface SearchFilters {
  category?: string;
  minPrice?: number;
  maxPrice?: number;
  inStock?: boolean;
  rating?: number;
  sortBy?: 'price-asc' | 'price-desc' | 'rating' | 'newest';
}

// features/product-search/model/search.model.ts
import { SearchState, SearchFilters } from './search.types';
import { Product } from '../../../entities/product/model/product.types';

export class SearchModel {
  private state: SearchState;

  constructor(initialState: Partial<SearchState> = {}) {
    this.state = {
      query: '',
      filters: {},
      results: [],
      isLoading: false,
      error: null,
      hasMore: true,
      page: 1,
      ...initialState,
    };
  }

  getState(): SearchState {
    return this.state;
  }

  setQuery(query: string) {
    this.state.query = query;
    this.state.page = 1;
    this.state.results = [];
  }

  setFilters(filters: SearchFilters) {
    this.state.filters = { ...this.state.filters, ...filters };
    this.state.page = 1;
    this.state.results = [];
  }

  setResults(results: Product[], append: boolean = false) {
    this.state.results = append
      ? [...this.state.results, ...results]
      : results;
  }

  setLoading(isLoading: boolean) {
    this.state.isLoading = isLoading;
  }

  setError(error: string | null) {
    this.state.error = error;
  }

  setHasMore(hasMore: boolean) {
    this.state.hasMore = hasMore;
  }

  incrementPage() {
    this.state.page += 1;
  }

  reset() {
    this.state = {
      query: '',
      filters: {},
      results: [],
      isLoading: false,
      error: null,
      hasMore: true,
      page: 1,
    };
  }

  getFilteredResults(): Product[] {
    return this.state.results.filter(product => {
      // Category filter
      if (this.state.filters.category &&
        product.category.id !== this.state.filters.category) {
        return false;
      }

      // Price range filter
      if (this.state.filters.minPrice !== undefined &&
        product.price < this.state.filters.minPrice) {
        return false;
      }

      if (this.state.filters.maxPrice !== undefined &&
        product.price > this.state.filters.maxPrice) {
        return false;
      }

      // In stock filter
      if (this.state.filters.inStock && product.stock === 0) {
        return false;
      }

      // Rating filter
      if (this.state.filters.rating !== undefined) {
        const avgRating = product.reviews.reduce((acc, r) => acc + r.rating, 0) / product.reviews.length;
        if (avgRating < this.state.filters.rating) {
          return false;
        }
      }

      return true;
    });
  }

  sortResults(results: Product[]): Product[] {
    const sortBy = this.state.filters.sortBy || 'newest';
    const sorted = [...results];

    switch (sortBy) {
      case 'price-asc':
        return sorted.sort((a, b) => a.price - b.price);
      case 'price-desc':
        return sorted.sort((a, b) => b.price - a.price);
      case 'rating':
        return sorted.sort((a, b) => {
          const ratingA = a.reviews.reduce((sum, r) => sum + r.rating, 0) / a.reviews.length;
          const ratingB = b.reviews.reduce((sum, r) => sum + r.rating, 0) / b.reviews.length;
          return ratingB - ratingA;
        });
      case 'newest':
      default:
        return sorted.sort((a, b) =>
          new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
        );
    }
  }
}

Feature Hook

// features/product-search/hooks/useSearch.ts
import { useQuery, useInfiniteQuery } from '@tanstack/react-query';
import { searchApi } from '../api/search.api';
import { SearchModel } from '../model/search.model';
import { useMemo, useCallback } from 'react';

export const useSearch = (initialQuery: string = '') => {
  const [searchState, setSearchState] = useState<SearchState>({
    query: initialQuery,
    filters: {},
    results: [],
    isLoading: false,
    error: null,
    hasMore: true,
    page: 1,
  });

  const searchModel = useMemo(() => new SearchModel(searchState), [searchState]);

  const { data, isLoading, error, fetchNextPage, hasNextPage } = useInfiniteQuery({
    queryKey: ['search', searchState.query, searchState.filters],
    queryFn: ({ pageParam = 1 }) =>
      searchApi.search(searchState.query, searchState.filters, pageParam),
    getNextPageParam: (lastPage, allPages) => {
      if (lastPage.length < 20) return undefined;
      return allPages.length + 1;
    },
    enabled: searchState.query.length >= 3,
    staleTime: 30000,
  });

  const results = useMemo(() => {
    if (!data) return [];
    return data.pages.flat();
  }, [data]);

  const performSearch = useCallback((query: string) => {
    searchModel.setQuery(query);
    setSearchState(searchModel.getState());
  }, [searchModel]);

  const applyFilters = useCallback((filters: SearchFilters) => {
    searchModel.setFilters(filters);
    setSearchState(searchModel.getState());
  }, [searchModel]);

  const loadMore = useCallback(() => {
    if (hasNextPage && !isLoading) {
      fetchNextPage();
    }
  }, [hasNextPage, isLoading, fetchNextPage]);

  return {
    results,
    isLoading,
    error: error?.message || null,
    performSearch,
    applyFilters,
    loadMore,
    hasMore: hasNextPage,
    model: searchModel,
  };
};

Widget

// widgets/product-list/ui/ProductList.tsx
import { useSearch } from '../../../features/product-search/hooks/useSearch';
import { ProductCard } from '../../../entities/product/ui/ProductCard';
import { SearchBar } from '../../../features/product-search/ui/SearchBar';
import { ProductFilters } from '../../../features/product-search/ui/ProductFilters';
import { LoadingSpinner } from '../../../shared/ui/atoms/Spinner';
import { EmptyState } from '../../../shared/ui/molecules/EmptyState';
import { useInView } from '../../../shared/hooks/useInView';

interface ProductListProps {
  initialQuery?: string;
  className?: string;
}

export const ProductList: React.FC<ProductListProps> = ({
  initialQuery = '',
  className,
}) => {
  const {
    results,
    isLoading,
    error,
    performSearch,
    applyFilters,
    loadMore,
    hasMore,
    model,
  } = useSearch(initialQuery);

  const { ref, inView } = useInView();

  // Auto-load more when reaching bottom
  useEffect(() => {
    if (inView && hasMore && !isLoading) {
      loadMore();
    }
  }, [inView, hasMore, isLoading, loadMore]);

  if (error) {
    return (
      <div className="product-list__error">
        <p>Error loading products: {error}</p>
        <Button onClick={() => performSearch(model.getState().query)}>
          Retry
        </Button>
      </div>
    );
  }

  return (
    <div className={classNames('product-list', className)}>
      <div className="product-list__header">
        <SearchBar
          onSearch={performSearch}
          initialValue={initialQuery}
          isLoading={isLoading}
        />
        <ProductFilters onApplyFilters={applyFilters} currentFilters={model.getState().filters} />
      </div>

      {isLoading && results.length === 0 ? (
        <div className="product-list__loading">
          <LoadingSpinner size="lg" />
          <p>Loading products...</p>
        </div>
      ) : results.length === 0 ? (
        <EmptyState
          title="No products found"
          description="Try adjusting your search or filters"
          action={
            <Button variant="secondary" onClick={() => performSearch('')}>
              Clear filters
            </Button>
          }
        />
      ) : (
        <>
          <div className="product-list__grid">
            {results.map((product, index) => (
              <ProductCard
                key={product.id}
                product={product}
                priority={index < 6}
              />
            ))}
          </div>

          {hasMore && (
            <div ref={ref} className="product-list__load-more">
              {isLoading && <LoadingSpinner size="sm" />}
            </div>
          )}

          {!hasMore && results.length > 0 && (
            <p className="product-list__end-message">
              You've reached the end of the results
            </p>
          )}
        </>
      )}
    </div>
  );
};

Page

// pages/products-page/ui/ProductsPage.tsx
import { ProductList } from '../../../widgets/product-list/ui/ProductList';
import { PageLayout } from '../../../widgets/page-layout/ui/PageLayout';
import { Breadcrumbs } from '../../../widgets/breadcrumbs/ui/Breadcrumbs';
import { PageTitle } from '../../../shared/ui/molecules/PageTitle';
import { useRouteParams } from '../../../shared/hooks/useRouteParams';

export const ProductsPage: React.FC = () => {
  const params = useRouteParams<{ category?: string }>();
  const category = params.category || '';

  return (
    <PageLayout>
      <div className="products-page">
        <Breadcrumbs
          items={[
            { label: 'Home', href: '/' },
            { label: 'Products', href: '/products' },
            ...(category ? [{ label: category, href: `/products/${category}` }] : []),
          ]}
        />

        <PageTitle
          title={category ? `${category} Products` : 'All Products'}
          subtitle="Browse our curated collection of high-quality products"
        />

        <ProductList
          initialQuery={category}
          className="products-page__list"
        />
      </div>
    </PageLayout>
  );
};

βœ… Pros

Advantage Description
Strict Layer Architecture Clear dependency rules (App β†’ Pages β†’ Widgets β†’ Features β†’ Entities β†’ Shared)
Excellent Scalability Designed for large applications from the start
High Reusability Components are reusable across features
Team Collaboration Clear ownership of slices
Maintainability Easy to understand where things belong

❌ Cons

Disadvantage Description
Steep Learning Curve Complex architecture to understand
Over-engineering Too much structure for small apps
Boilerplate Lots of folders and files
Rigidity Can be inflexible for some use cases

🎯 Best For


4. Hybrid Approach

🎯 Philosophy

β€œBalance technical organization with feature cohesion”

The Hybrid approach organizes code by technical concerns at the top level for easy navigation, while grouping related business logic within feature folders to maintain cohesion. It strikes a practical balance between rigid architecture and complete flexibility.

πŸ“‚ Complete Structure

src/
β”œβ”€β”€ assets/                            # Static assets
β”‚   β”œβ”€β”€ images/
β”‚   β”œβ”€β”€ fonts/
β”‚   └── icons/
β”‚
β”œβ”€β”€ components/                        # Reusable components
β”‚   β”œβ”€β”€ Button/
β”‚   β”‚   β”œβ”€β”€ Button.tsx
β”‚   β”‚   β”œβ”€β”€ Button.module.scss
β”‚   β”‚   └── Button.test.tsx
β”‚   β”œβ”€β”€ Modal/
β”‚   β”‚   β”œβ”€β”€ Modal.tsx
β”‚   β”‚   └── Modal.module.scss
β”‚   β”œβ”€β”€ Navbar/
β”‚   β”‚   β”œβ”€β”€ Navbar.tsx
β”‚   β”‚   └── Navbar.module.scss
β”‚   β”œβ”€β”€ Card/
β”‚   β”‚   β”œβ”€β”€ Card.tsx
β”‚   β”‚   └── Card.module.scss
β”‚   └── index.ts
β”‚
β”œβ”€β”€ features/                          # Feature-specific logic
β”‚   β”œβ”€β”€ auth/
β”‚   β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”‚   β”œβ”€β”€ LoginForm.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ RegisterForm.tsx
β”‚   β”‚   β”‚   └── AuthGuard.tsx
β”‚   β”‚   β”œβ”€β”€ hooks/
β”‚   β”‚   β”‚   └── useAuth.ts
β”‚   β”‚   β”œβ”€β”€ services/
β”‚   β”‚   β”‚   └── auth.service.ts
β”‚   β”‚   β”œβ”€β”€ types/
β”‚   β”‚   β”‚   └── auth.types.ts
β”‚   β”‚   β”œβ”€β”€ store/
β”‚   β”‚   β”‚   └── auth.slice.ts
β”‚   β”‚   └── index.ts
β”‚   β”‚
β”‚   β”œβ”€β”€ dashboard/
β”‚   β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”‚   β”œβ”€β”€ DashboardStats.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ ActivityFeed.tsx
β”‚   β”‚   β”‚   └── ChartWidget.tsx
β”‚   β”‚   β”œβ”€β”€ hooks/
β”‚   β”‚   β”‚   └── useDashboard.ts
β”‚   β”‚   β”œβ”€β”€ services/
β”‚   β”‚   β”‚   └── dashboard.service.ts
β”‚   β”‚   β”œβ”€β”€ types/
β”‚   β”‚   β”‚   └── dashboard.types.ts
β”‚   β”‚   └── index.ts
β”‚   β”‚
β”‚   β”œβ”€β”€ profile/
β”‚   β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”‚   β”œβ”€β”€ UserProfile.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ ProfileSettings.tsx
β”‚   β”‚   β”‚   └── AvatarUpload.tsx
β”‚   β”‚   β”œβ”€β”€ hooks/
β”‚   β”‚   β”‚   └── useProfile.ts
β”‚   β”‚   β”œβ”€β”€ services/
β”‚   β”‚   β”‚   └── profile.service.ts
β”‚   β”‚   β”œβ”€β”€ types/
β”‚   β”‚   β”‚   └── profile.types.ts
β”‚   β”‚   └── index.ts
β”‚   β”‚
β”‚   └── products/
β”‚       β”œβ”€β”€ components/
β”‚       β”‚   β”œβ”€β”€ ProductCard.tsx
β”‚       β”‚   β”œβ”€β”€ ProductList.tsx
β”‚       β”‚   └── ProductFilters.tsx
β”‚       β”œβ”€β”€ hooks/
β”‚       β”‚   β”œβ”€β”€ useProducts.ts
β”‚       β”‚   └── useProductSearch.ts
β”‚       β”œβ”€β”€ services/
β”‚       β”‚   └── product.service.ts
β”‚       β”œβ”€β”€ types/
β”‚       β”‚   └── product.types.ts
β”‚       └── index.ts
β”‚
β”œβ”€β”€ hooks/                             # Global custom hooks
β”‚   β”œβ”€β”€ useAuth.ts
β”‚   β”œβ”€β”€ useFetch.ts
β”‚   β”œβ”€β”€ useForm.ts
β”‚   β”œβ”€β”€ useLocalStorage.ts
β”‚   β”œβ”€β”€ useDebounce.ts
β”‚   └── useMediaQuery.ts
β”‚
β”œβ”€β”€ layouts/                           # Layout components
β”‚   β”œβ”€β”€ MainLayout/
β”‚   β”‚   β”œβ”€β”€ MainLayout.tsx
β”‚   β”‚   └── MainLayout.module.scss
β”‚   β”œβ”€β”€ AdminLayout/
β”‚   β”‚   β”œβ”€β”€ AdminLayout.tsx
β”‚   β”‚   └── AdminLayout.module.scss
β”‚   └── AuthLayout/
β”‚       β”œβ”€β”€ AuthLayout.tsx
β”‚       └── AuthLayout.module.scss
β”‚
β”œβ”€β”€ pages/                             # Page components (routes)
β”‚   β”œβ”€β”€ auth/
β”‚   β”‚   β”œβ”€β”€ LoginPage.tsx
β”‚   β”‚   β”œβ”€β”€ RegisterPage.tsx
β”‚   β”‚   └── ForgotPasswordPage.tsx
β”‚   β”œβ”€β”€ dashboard/
β”‚   β”‚   └── DashboardPage.tsx
β”‚   β”œβ”€β”€ products/
β”‚   β”‚   β”œβ”€β”€ ProductsPage.tsx
β”‚   β”‚   β”œβ”€β”€ ProductDetailsPage.tsx
β”‚   β”‚   └── ProductCreatePage.tsx
β”‚   β”œβ”€β”€ users/
β”‚   β”‚   β”œβ”€β”€ UsersPage.tsx
β”‚   β”‚   └── UserDetailsPage.tsx
β”‚   └── index.ts
β”‚
β”œβ”€β”€ services/                          # API and external services
β”‚   β”œβ”€β”€ api/
β”‚   β”‚   β”œβ”€β”€ apiClient.ts
β”‚   β”‚   β”œβ”€β”€ api.interceptors.ts
β”‚   β”‚   └── endpoints.ts
β”‚   β”œβ”€β”€ authService.ts
β”‚   β”œβ”€β”€ userService.ts
β”‚   β”œβ”€β”€ productService.ts
β”‚   └── index.ts
β”‚
β”œβ”€β”€ store/                             # Global state management
β”‚   β”œβ”€β”€ slices/
β”‚   β”‚   β”œβ”€β”€ auth.slice.ts
β”‚   β”‚   β”œβ”€β”€ user.slice.ts
β”‚   β”‚   β”œβ”€β”€ product.slice.ts
β”‚   β”‚   └── ui.slice.ts
β”‚   β”œβ”€β”€ selectors/
β”‚   β”‚   β”œβ”€β”€ auth.selectors.ts
β”‚   β”‚   β”œβ”€β”€ user.selectors.ts
β”‚   β”‚   └── product.selectors.ts
β”‚   β”œβ”€β”€ hooks/
β”‚   β”‚   β”œβ”€β”€ useAppDispatch.ts
β”‚   β”‚   └── useAppSelector.ts
β”‚   └── store.ts
β”‚
β”œβ”€β”€ styles/                            # Global styles
β”‚   β”œβ”€β”€ abstracts/
β”‚   β”‚   β”œβ”€β”€ _variables.scss
β”‚   β”‚   β”œβ”€β”€ _mixins.scss
β”‚   β”‚   └── _functions.scss
β”‚   β”œβ”€β”€ base/
β”‚   β”‚   β”œβ”€β”€ _reset.scss
β”‚   β”‚   └── _typography.scss
β”‚   β”œβ”€β”€ components/
β”‚   β”‚   └── _utilities.scss
β”‚   β”œβ”€β”€ themes/
β”‚   β”‚   β”œβ”€β”€ _light.scss
β”‚   β”‚   └── _dark.scss
β”‚   └── global.scss
β”‚
β”œβ”€β”€ types/                             # TypeScript types
β”‚   β”œβ”€β”€ api.types.ts
β”‚   β”œβ”€β”€ common.types.ts
β”‚   β”œβ”€β”€ user.types.ts
β”‚   └── component.types.ts
β”‚
β”œβ”€β”€ utils/                             # Utility functions
β”‚   β”œβ”€β”€ formatters/
β”‚   β”‚   β”œβ”€β”€ date.formatters.ts
β”‚   β”‚   β”œβ”€β”€ currency.formatters.ts
β”‚   β”‚   └── string.formatters.ts
β”‚   β”œβ”€β”€ validators/
β”‚   β”‚   β”œβ”€β”€ email.validators.ts
β”‚   β”‚   └── password.validators.ts
β”‚   β”œβ”€β”€ helpers/
β”‚   β”‚   β”œβ”€β”€ debounce.ts
β”‚   β”‚   └── throttle.ts
β”‚   └── index.ts
β”‚
β”œβ”€β”€ constants/                         # Constants
β”‚   β”œβ”€β”€ routes.ts
β”‚   β”œβ”€β”€ roles.ts
β”‚   β”œβ”€β”€ permissions.ts
β”‚   └── app.constants.ts
β”‚
β”œβ”€β”€ config/                            # Configuration
β”‚   β”œβ”€β”€ env.ts
β”‚   β”œβ”€β”€ app.config.ts
β”‚   └── theme.config.ts
β”‚
β”œβ”€β”€ App.tsx
β”œβ”€β”€ index.tsx
└── router.tsx

πŸ’» Example Code

Shared Component

// components/Button/Button.tsx
export interface ButtonProps {
  variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
  children: React.ReactNode;
  onClick?: () => void;
  loading?: boolean;
  disabled?: boolean;
  fullWidth?: boolean;
  className?: string;
  type?: 'button' | 'submit' | 'reset';
  ariaLabel?: string;
}

export const Button: React.FC<ButtonProps> = ({
  variant = 'primary',
  size = 'md',
  children,
  onClick,
  loading = false,
  disabled = false,
  fullWidth = false,
  className = '',
  type = 'button',
  ariaLabel,
}) => {
  return (
    <button
      className={`btn btn--${variant} btn--${size} ${fullWidth ? 'btn--full-width' : ''} ${className}`}
      onClick={onClick}
      disabled={disabled || loading}
      type={type}
      aria-label={ariaLabel || (typeof children === 'string' ? children : undefined)}
      aria-busy={loading}
    >
      {loading && <Spinner size="sm" className="btn__spinner" />}
      <span className="btn__content">{children}</span>
    </button>
  );
};

Feature Service

// features/auth/services/auth.service.ts
import { apiClient } from '../../../services/api/apiClient';
import { User, LoginCredentials, RegisterData, AuthResponse } from '../types/auth.types';

class AuthService {
  private static instance: AuthService;

  static getInstance(): AuthService {
    if (!AuthService.instance) {
      AuthService.instance = new AuthService();
    }
    return AuthService.instance;
  }

  async login(credentials: LoginCredentials): Promise<User> {
    try {
      const response = await apiClient.post<AuthResponse>('/auth/login', credentials);
      const { user, token } = response.data;

      // Store token
      localStorage.setItem('access_token', token);
      apiClient.defaults.headers.common['Authorization'] = `Bearer ${token}`;

      return user;
    } catch (error) {
      throw this.handleError(error);
    }
  }

  async register(data: RegisterData): Promise<User> {
    try {
      const response = await apiClient.post<AuthResponse>('/auth/register', data);
      return response.data.user;
    } catch (error) {
      throw this.handleError(error);
    }
  }

  async logout(): Promise<void> {
    try {
      await apiClient.post('/auth/logout');
    } finally {
      localStorage.removeItem('access_token');
      delete apiClient.defaults.headers.common['Authorization'];
    }
  }

  async refreshToken(): Promise<string> {
    try {
      const refreshToken = localStorage.getItem('refresh_token');
      const response = await apiClient.post<{ token: string }>('/auth/refresh', {
        refreshToken,
      });
      const { token } = response.data;
      localStorage.setItem('access_token', token);
      return token;
    } catch (error) {
      throw this.handleError(error);
    }
  }

  async getCurrentUser(): Promise<User> {
    try {
      const response = await apiClient.get<User>('/auth/me');
      return response.data;
    } catch (error) {
      throw this.handleError(error);
    }
  }

  private handleError(error: any): Error {
    if (error.response) {
      const message = error.response.data?.message || 'An error occurred';
      return new Error(message);
    }
    return new Error('Network error');
  }
}

export const authService = AuthService.getInstance();

Feature Hook

// features/auth/hooks/useAuth.ts
import { useState, useCallback, useEffect } from 'react';
import { authService } from '../services/auth.service';
import { useAppDispatch, useAppSelector } from '../../../store/hooks';
import { setUser, clearUser, setLoading, setError } from '../store/auth.slice';
import { User, LoginCredentials, RegisterData } from '../types/auth.types';

export const useAuth = () => {
  const dispatch = useAppDispatch();
  const { user, isAuthenticated, isLoading, error } = useAppSelector(
    (state) => state.auth
  );

  const login = useCallback(async (credentials: LoginCredentials) => {
    try {
      dispatch(setLoading(true));
      dispatch(setError(null));
      const user = await authService.login(credentials);
      dispatch(setUser(user));
      return user;
    } catch (error) {
      dispatch(setError(error.message));
      throw error;
    } finally {
      dispatch(setLoading(false));
    }
  }, [dispatch]);

  const register = useCallback(async (data: RegisterData) => {
    try {
      dispatch(setLoading(true));
      dispatch(setError(null));
      const user = await authService.register(data);
      return user;
    } catch (error) {
      dispatch(setError(error.message));
      throw error;
    } finally {
      dispatch(setLoading(false));
    }
  }, [dispatch]);

  const logout = useCallback(async () => {
    try {
      await authService.logout();
    } finally {
      dispatch(clearUser());
    }
  }, [dispatch]);

  const refreshSession = useCallback(async () => {
    try {
      const token = await authService.refreshToken();
      const user = await authService.getCurrentUser();
      dispatch(setUser(user));
      return token;
    } catch (error) {
      dispatch(clearUser());
      throw error;
    }
  }, [dispatch]);

  // Check session on mount
  useEffect(() => {
    const token = localStorage.getItem('access_token');
    if (token && !user) {
      refreshSession().catch(() => {
        // Silent fail - session will be checked on next route change
      });
    }
  }, [user, refreshSession]);

  return {
    user,
    isAuthenticated,
    isLoading,
    error,
    login,
    register,
    logout,
    refreshSession,
  };
};

Feature Component

// features/auth/components/LoginForm.tsx
import { useState } from 'react';
import { useAuth } from '../hooks/useAuth';
import { Button } from '../../../components/Button/Button';
import { Input } from '../../../components/Input/Input';
import { useNavigate, Link } from 'react-router-dom';
import { validateEmail, validatePassword } from '../../../utils/validators';

interface LoginFormProps {
  redirectTo?: string;
  onSuccess?: () => void;
}

export const LoginForm: React.FC<LoginFormProps> = ({
  redirectTo = '/dashboard',
  onSuccess,
}) => {
  const navigate = useNavigate();
  const { login, isLoading, error } = useAuth();
  const [formData, setFormData] = useState({
    email: '',
    password: '',
  });
  const [formErrors, setFormErrors] = useState({
    email: '',
    password: '',
  });

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));
    // Clear error on change
    setFormErrors(prev => ({ ...prev, [name]: '' }));
  };

  const validate = (): boolean => {
    const errors = {
      email: validateEmail(formData.email) ? '' : 'Please enter a valid email address',
      password: validatePassword(formData.password) ? '' : 'Password must be at least 8 characters',
    };
    setFormErrors(errors);
    return !errors.email && !errors.password;
  };

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    if (!validate()) {
      return;
    }

    try {
      await login(formData);
      onSuccess?.();
      navigate(redirectTo);
    } catch (error) {
      // Error is handled by useAuth
    }
  };

  return (
    <form className="login-form" onSubmit={handleSubmit} noValidate>
      <div className="login-form__header">
        <h2>Welcome Back</h2>
        <p>Sign in to your account to continue</p>
      </div>

      {error && (
        <div className="login-form__error" role="alert">
          {error}
        </div>
      )}

      <Input
        type="email"
        name="email"
        value={formData.email}
        onChange={handleChange}
        label="Email Address"
        placeholder="you@example.com"
        error={formErrors.email}
        disabled={isLoading}
        required
        autoFocus
      />

      <Input
        type="password"
        name="password"
        value={formData.password}
        onChange={handleChange}
        label="Password"
        placeholder="Enter your password"
        error={formErrors.password}
        disabled={isLoading}
        required
      />

      <div className="login-form__actions">
        <Button
          type="submit"
          variant="primary"
          size="lg"
          loading={isLoading}
          disabled={isLoading}
          fullWidth
        >
          {isLoading ? 'Signing in...' : 'Sign In'}
        </Button>
      </div>

      <div className="login-form__footer">
        <Link to="/forgot-password" className="login-form__link">
          Forgot password?
        </Link>
        <span className="login-form__separator">|</span>
        <Link to="/register" className="login-form__link">
          Create account
        </Link>
      </div>
    </form>
  );
};

Page

// pages/auth/LoginPage.tsx
import { LoginForm } from '../../features/auth/components/LoginForm';
import { AuthLayout } from '../../layouts/AuthLayout/AuthLayout';

export const LoginPage: React.FC = () => {
  return (
    <AuthLayout>
      <div className="login-page">
        <div className="login-page__container">
          <div className="login-page__brand">
            <img src="/logo.svg" alt="Company Logo" />
            <h1>Company Name</h1>
          </div>
          <LoginForm redirectTo="/dashboard" />
        </div>
      </div>
    </AuthLayout>
  );
};

βœ… Pros

Advantage Description
Familiar Most React developers understand this structure
Flexible Easy to modify and adapt as requirements change
Balanced Good mix of technical and feature organization
Low Learning Curve Easy for new developers to understand
Scalable Can grow with the application

❌ Cons

Disadvantage Description
Less Opinionated Can become messy without discipline
Feature Scattering Auth logic spread across multiple folders
Co-location Issues Related code not always together
Requires Discipline Team needs to agree on conventions

🎯 Best For


Comparison Table

Aspect Atomic Design Domain-Driven Design Feature-Sliced Design Hybrid
Primary Focus UI Design System Business Domains Feature Boundaries Balanced
Learning Curve 🟒 Easy 🟑 Medium πŸ”΄ Hard 🟒 Easy
Scalability 🟑 Medium 🟒 High 🟒 Very High 🟑 Medium-High
Team Collaboration 🟒 High 🟒 Very High 🟒 Very High 🟑 Medium
Code Reusability 🟒 Very High 🟑 Medium 🟒 High 🟑 Medium
Separation of Concerns 🟑 Medium 🟒 High 🟒 Very High 🟑 Medium
Boilerplate 🟑 Medium πŸ”΄ High πŸ”΄ Very High 🟒 Low
Flexibility 🟑 Medium 🟒 High πŸ”΄ Low 🟒 Very High
Best for App Size Small-Medium Medium-Large Large-Enterprise Small-Medium
Component Reusability 🟒 Excellent 🟑 Good 🟒 Excellent 🟑 Good
Business Logic Organization 🟑 Average 🟒 Excellent 🟒 Excellent 🟑 Average
Maintainability 🟑 Good 🟒 Very Good 🟒 Excellent 🟑 Good
Testing Ease 🟒 Easy 🟑 Medium 🟒 Easy 🟑 Medium

Which Approach Should You Choose?

🎯 Choose Atomic Design if:

🎯 Choose Domain-Driven Design if:

🎯 Choose Feature-Sliced Design if:

🎯 Choose Hybrid if:


πŸš€ Recommendation

I recommend a combination approach for most React projects:

  1. Start with the Hybrid structure for its simplicity and familiarity
  2. Apply Atomic Design principles to your /components/ folder for consistency
  3. Use Feature-based organization for /features/ with clear boundaries
  4. Gradually adopt DDD if the business logic becomes complex

Example: Combining Approaches

/src/
  /components/          # Atomic Design principles here
    /atoms/
    /molecules/
    /organisms/
  
  /features/           # Feature-Sliced inspired
    /auth/             # DDD domain approach
      /components/
      /hooks/
      /services/
      /types/
  
  /shared/             # Shared utilities
    /ui/
    /utils/
    /constants/

Why This Works

Getting Started

  1. Start Simple: Begin with the Hybrid approach
  2. Refine as You Grow: Add more structure as needed
  3. Stay Consistent: Document your conventions
  4. Review Regularly: Adapt as the project evolves

πŸ“š Additional Resources


πŸ“ License

MIT Β© 2026 - Feel free to use and adapt

πŸ’‘ Feedback

If you have suggestions or improvements, please contribute!

See Contributing Guidelines

</a> GitHub Bluesky