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.
β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.
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
// 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>
);
};
// 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>
);
};
// 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>
);
};
| 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 |
| 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 |
β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.
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/
// 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;
}
// 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;
}
}
// 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,
};
};
// 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>
);
};
| 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 |
| 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 |
β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.
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
// 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>
);
};
// 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);
},
};
// 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()
);
}
}
}
// 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,
};
};
// 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>
);
};
// 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>
);
};
| 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 |
| 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 |
β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.
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
// 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>
);
};
// 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();
// 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,
};
};
// 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>
);
};
// 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>
);
};
| 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 |
| 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 |
| 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 |
I recommend a combination approach for most React projects:
/components/ folder for consistency/features/ with clear boundaries/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/
MIT Β© 2026 - Feel free to use and adapt
If you have suggestions or improvements, please contribute!