A Battle-Tested Way to Structure React Projects
Learn how to structure your React projects with a focus on scalability, maintainability, and team collaboration.
Published: 03 Mar, 2025
In large-scale React projects, I structure components with a focus on scalability, maintainability, and team collaboration. Here’s my battle-tested approach:
Atomic Design + Feature-Based Hybrid Structure
src/
├── core/ # Global system components
│ ├── design-system/ # Atomic design implementation
│ │ ├── atoms/
│ │ ├── molecules/
│ │ └── organisms/
│ ├── providers/ # All context providers
│ ├── hooks/ # Reusable custom hooks
│ └── utils/ # Shared utilities
│
├── features/ # Feature-based modules
│ ├── cart/
│ │ ├── components/ # Feature-specific components
│ │ ├── hooks/ # Feature-specific hooks
│ │ ├── types/ # Feature-specific TS types
│ │ └── index.ts # Clean public API
│ └── user-profile/
│
├── app/ # Main app structure
│ ├── layouts/ # Layout components
│ └── routes/ # Route configuration
│
├── assets/ # Static assets
└── lib/ # Third-party integrations/adapters
Component Categorization Strategy
- Core Components: Reusable UI building blocks (Buttons, Inputs)
- Domain Components: Business logic components (ProductCard, PaymentForm)
- Container Components: State management + data fetching
- Layout Components: Page structure providers
- HOC Components: Cross-cutting concerns (auth, logging)
Strict Component Contracts
// ComponentName.tsx
interface ComponentNameProps {
variant: "primary" | "secondary";
children: React.ReactNode;
/** @default false */
disabled?: boolean;
}
export const ComponentName = ({
variant = "primary",
children,
disabled = false,
}: ComponentNameProps) => {
// Component implementation
};
State Management Hierarchy
- Local State: useState/useReducer for UI state
- Component Group State: Context API for medium-scoped state
- Global State: Jotai for app-wide state
- Server State: TanStack Query for async data
Performance Architecture
- Code splitting at route level with
React.lazy()
- Memoization strategy:
React.memo
for component memoizationuseMemo
for expensive calculationsuseCallback
for stable function references
- Virtualization for large lists (react-virtual)
Testing Strategy
- Unit Tests: Jest + Testing Library (75% coverage minimum)
- Integration Tests: Playwright for critical user flows
- Visual Tests: Chromatic/Storybook for UI consistency
- E2E Tests: Cypress for key business journeys
Documentation System
- Storybook for component documentation
- TypeDoc for TypeScript type documentation
- ADRs (Architectural Decision Records) for major decisions
CONTRIBUTING.md
with component patterns and anti-patterns
Dependency Management
- Internal component library package (@company/ui)
- Dedicated API client layer
- Strict dependency injection pattern:
// Instead of direct imports
import { api } from "core/http";
// Use dependency injection
const MyComponent = ({ httpClient }: { httpClient: HttpClient }) => {
// Component implementation
};
Onboarding Infrastructure
- Component playground environment
- CLI scaffolding tool for new features
- Visual regression testing dashboard
- Interactive architecture diagram (Mermaid/FigJam)
Continuous Refactoring Process
- Bi-weekly tech debt sprints
- Automated code smell detection (SonarQube)
- TypeScript strict mode enforcement
- Deprecation policy for legacy components
Conclusion
This structure has proven effective across multiple enterprise React codebases (100k+ LOC). The key is maintaining strict boundaries between layers while allowing flexibility within feature modules. We enforce this architecture through:
ESLint import/export rules CI pipeline checks Monorepo strategy (when applicable) Automated dependency graph analysis
The goal is to create a system where developers can work in isolation on features while maintaining consistent patterns across the entire application.