React Component Architecture: Composition Patterns for Scale
Problem Statement
React applications often struggle with maintainability as they grow, leading to prop drilling, tightly coupled components, and code duplication. Engineers frequently find themselves refactoring entire component trees to add features, modify layouts, or share functionality. These architectural issues lead to increased development time, inconsistent UIs, and difficulty onboarding new team members.
Solution Overview
Effective React component architecture uses composition patterns to create flexible, maintainable component hierarchies that can adapt to changing requirements. By implementing the right component patterns, you can build UIs that are both modular and cohesive.
The right architecture allows your components to be reused, composed, and extended with minimal effort, even as application requirements evolve.
Implementation Details
1. Component Hierarchies and Types
Establishing a clear component hierarchy helps teams make consistent decisions about component boundaries and responsibilities.
UI Components (Presentational)
Focus on how things look, receive data via props, and rarely have internal state.
1// Simple, reusable UI component 2const Button = ({ children, variant = 'primary', size = 'medium', onClick }) => { 3 const baseClasses = 'rounded font-medium transition-colors'; 4 5 const variantClasses = { 6 primary: 'bg-blue-600 text-white hover:bg-blue-700', 7 secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300', 8 danger: 'bg-red-600 text-white hover:bg-red-700' 9 }; 10
Implementation tip: Keep UI components pure and focused on rendering. They should work with any data that matches their prop interface.
Container Components
Handle data fetching, state management, and pass data to child components.
1// Container component handling data fetching 2const UserProfileContainer = ({ userId }) => { 3 const [user, setUser] = useState(null); 4 const [loading, setLoading] = useState(true); 5 const [error, setError] = useState(null); 6 7 useEffect(() => { 8 const fetchUser = async () => { 9 setLoading(true); 10 try {
Implementation tip: Separate data fetching from presentation to enable independent testing and easier refactoring.
Layout Components
Define the structure of the page and positioning of elements.
1// Layout component for page structure 2const DashboardLayout = ({ sidebar, main, header }) => { 3 return ( 4 <div className="flex h-screen overflow-hidden"> 5 <header className="fixed top-0 w-full h-16 z-10 bg-white shadow"> 6 {header} 7 </header> 8 <div className="flex mt-16 h-[calc(100vh-4rem)]"> 9 <aside className="w-64 overflow-y-auto bg-gray-100"> 10 {sidebar}
Implementation tip: Use composition via children or named props rather than configuration objects for maximum flexibility.
2. Solving Prop Drilling with Composition
Prop drilling occurs when props need to be passed through multiple layers of components.
Problem: Prop Drilling
1// Problematic prop drilling example 2const App = () => { 3 const [theme, setTheme] = useState('light'); 4 5 return ( 6 <Page theme={theme} setTheme={setTheme}> 7 <Header theme={theme} setTheme={setTheme} /> 8 <Content theme={theme} /> 9 <Footer theme={theme} setTheme={setTheme} /> 10 </Page>
Solution 1: Context API
For widely used values like themes, user data, or feature flags:
1// Create context 2const ThemeContext = createContext(); 3 4// Provider component 5const ThemeProvider = ({ children }) => { 6 const [theme, setTheme] = useState('light'); 7 const toggleTheme = () => setTheme(theme === 'light' ? 'dark' : 'light'); 8 9 // Memoize value to prevent unnecessary renders 10 const value = useMemo(() => ({
Solution 2: Compound Components
For groups of related components that share state:
1// Compound components using React.Children.map 2const Tabs = ({ children, defaultTab }) => { 3 const [activeTab, setActiveTab] = useState(defaultTab); 4 5 const clonedChildren = React.Children.map(children, child => { 6 if (child.type === Tab) { 7 return React.cloneElement(child, { 8 isActive: activeTab === child.props.id, 9 onActivate: () => setActiveTab(child.props.id) 10 });
Solution 3: Render Props
For sharing stateful logic between components:
1// Render props pattern for form validation 2const FormValidator = ({ initialValues, validationSchema, onSubmit, children }) => { 3 const [values, setValues] = useState(initialValues); 4 const [errors, setErrors] = useState({}); 5 const [touched, setTouched] = useState({}); 6 7 const handleChange = (e) => { 8 const { name, value } = e.target; 9 setValues({ ...values, [name]: value }); 10 };
Implementation tip: For complex cases, combine multiple patterns. For example, use Context for global state and Compound Components for local component interactions.
3. Custom Hooks for Logic Reuse
Extract component logic into custom hooks for reuse across components:
1// Custom hook for fetching and pagination 2const usePaginatedData = (fetchFunction, itemsPerPage = 10) => { 3 const [data, setData] = useState([]); 4 const [loading, setLoading] = useState(false); 5 const [error, setError] = useState(null); 6 const [page, setPage] = useState(1); 7 const [totalPages, setTotalPages] = useState(0); 8 9 useEffect(() => { 10 const fetchData = async () => {
Implementation tip: Hooks should be focused on a single responsibility, making them easier to compose and reuse.
4. Component Composition with Higher-Order Components (HOCs)
HOCs can add functionality to multiple components by wrapping them:
1// HOC for adding authentication checks 2const withAuthProtection = (Component) => { 3 const AuthProtected = (props) => { 4 const { isAuthenticated, isLoading } = useAuth(); 5 6 if (isLoading) { 7 return <LoadingSpinner />; 8 } 9 10 if (!isAuthenticated) {
Implementation tip: While HOCs are less common in modern React, they're still useful for cross-cutting concerns like authorization, analytics, or feature flags.
Real Interview Questions & Solutions
Question 1: Component Structure and Composition (Meta)
Problem: Design a reusable form system for an e-commerce website that handles different types of forms (login, registration, checkout) while maximizing code reuse and maintainability.
Interviewer's focus: Testing your ability to design component hierarchies and balance flexibility with standardization.
Approach:
1// Form component system with composition 2// 1. Base components for UI primitives 3const FormInput = ({ id, label, error, ...props }) => ( 4 <div className="form-field"> 5 <label htmlFor={id}>{label}</label> 6 <input id={id} {...props} className={error ? 'input-error' : ''} /> 7 {error && <div className="error-message">{error}</div>} 8 </div> 9); 10
Key insight: Separating concerns into UI components, form logic, and specific form implementations provides flexibility while maintaining consistency. Using the children prop with React.Children allows for declarative form structure with automatic state and validation handling.
Question 2: Building a Flexible Card Component (Amazon)
Problem: Design a Card component that's flexible enough to display different types of content (products, articles, user profiles) while maintaining consistent styling and behavior.
Interviewer's focus: Evaluating your approach to component API design and composition flexibility.
1// Flexible Card component system using composition 2// 1. Base Card component 3const Card = ({ children, className, onClick }) => ( 4 <div 5 className={`card ${className || ''}`} 6 onClick={onClick} 7 > 8 {children} 9 </div> 10);
Key insight: Use composition to create a family of card components that can be combined in different ways. This allows for consistent styling while enabling specialized cards for different content types. The component API is both flexible and intuitive for developers.
Question 3: Component Organization Strategy (Google)
Problem: How would you organize components in a large-scale React application? Design a component structure for an e-commerce platform with multiple user types (customers, merchants, admins) and various feature areas.
Interviewer's focus: Assessing your ability to organize code in a maintainable way that scales with team size.
Solution:
Implementation approach:
-
Feature-first organization with atomic design principles:
- Components are organized by feature domain (
products
,checkout
,admin
) rather than by component type - Each feature contains its own components, containers, and hooks
- Common/shared UI elements are in a central component library
- Components are organized by feature domain (
-
Component categorization:
- Common components: UI primitives used across features (buttons, inputs, cards, etc.)
- Feature components: Domain-specific components tied to a particular feature
- Container components: Handle data fetching and state management
- Layout components: Define page structure and positioning
-
Feature module structure:
features/products/ ├── components/ # Presentational components │ ├── ProductCard.jsx │ ├── ProductGrid.jsx │ └── ProductFilters.jsx ├── containers/ # Data-fetching containers │ ├── ProductListContainer.jsx │ └── ProductDetailContainer.jsx ├── hooks/ # Feature-specific hooks │ └── useProductSearch.js ├── utils/ # Feature-specific utilities │ └── productFormatters.js ├── types/ # TypeScript types/interfaces │ └── product.types.ts └── index.js # Public API for the feature
-
Shared hooks and utilities:
- Custom hooks for reusable logic (
useForm
,usePagination
, etc.) - Utility functions for common operations
- API clients and services
- Custom hooks for reusable logic (
Key insight: Organize components by feature domain for better code locality and maintainability. This approach scales better with larger teams as each feature area can evolve independently. Common UI components should be extracted to a shared library for consistency.
Question 4: Component API Design (Apple)
Problem: Design a Table component with a flexible API that handles sorting, filtering, pagination, and custom cell rendering.
Interviewer's focus: Evaluating your ability to design component APIs that balance flexibility, usability, and maintainability.
1// Table component with flexible API 2const Table = ({ 3 data, 4 columns, 5 pagination, 6 sorting, 7 filtering, 8 onRowClick, 9 rowClassName, 10 emptyState
Key insight: Design component APIs to be declarative rather than imperative. Use objects to group related props, and provide sensible defaults. Support both simple use cases and complex customization through render props and callback functions.
Results & Validation
Component Architecture Comparison
Architecture Approach | Maintainability | Reusability | Performance | Team Scalability |
---|---|---|---|---|
Monolithic Components | Poor | Poor | Poor | Poor |
UI/Container Split | Good | Good | Good | Good |
Feature-Based | Very Good | Good | Very Good | Excellent |
Atomic Design | Excellent | Excellent | Good | Good |
Pattern Library | Excellent | Excellent | Very Good | Very Good |
Based on evaluation of component architecture approaches in large-scale React applications
Real-World Application
A large e-commerce platform refactored their component architecture from a mixed approach to a consistent feature-based organization with a shared component library.
Results:
- 40% reduction in duplicate component code
- 35% faster onboarding time for new developers
- 50% fewer bugs related to component integration
- Improved velocity for feature development due to clearer boundaries
Trade-offs and Limitations
- Flexibility vs. Standardization: More flexible components often require more complex APIs and implementation details
- Reusability vs. Specificity: Highly reusable components may lack specific features needed for particular use cases
- Component Size vs. Composition: Smaller, more focused components increase composition complexity but improve reusability
- Performance vs. Abstraction: Higher levels of abstraction can sometimes impact performance if not carefully implemented
Key Takeaways
- Composition Over Configuration: Use component composition to create flexible, reusable UI elements that can adapt to different requirements
- Establish Clear Component Types: Define clear boundaries between UI components, container components, and layout components
- Design Intuitive Component APIs: Component props should be intuitive, with sensible defaults and consistent patterns
- Leverage Custom Hooks: Extract reusable logic into custom hooks to share behavior between components
- Organize by Feature Domain: Group related components, containers, and hooks by feature area for better code locality
Component Architecture Decision Framework
Download our comprehensive framework for designing scalable React component architectures.
The framework includes:
- Component hierarchy templates for different application types
- API design patterns and best practices
- Decision trees for splitting components
- Component organization strategies
- Code snippets for common composition patterns