Chapter 2: Component Architecture - Building Scalable React Applications
Welcome to the comprehensive guide to React component architecture! In this chapter, we'll explore advanced component patterns, composition techniques, and architectural principles that will help you build scalable, maintainable, and performant React applications.
Learning Objectives
By the end of this chapter, you will understand:
- What component architecture means and why it's crucial for React applications
- How to design components for maximum reusability and maintainability
- Why different component patterns exist and when to use each one
- What composition patterns are and how they solve common problems
- How to avoid common pitfalls like props drilling and tight coupling
- Why performance matters in component design and how to optimize it
- What modern React patterns are and how they improve development experience
What is Component Architecture? The Foundation of Scalable React Apps
What is Component Architecture Exactly?
Component architecture in React refers to the systematic approach of designing, organizing, and structuring components to create maintainable, scalable, and reusable applications. It's about making smart decisions about how components interact, share data, and compose together.
Component architecture is what separates good React applications from great ones. It's not just about writing components that work, but about creating a system of components that work well together, are easy to maintain, and can grow with your application.
What Makes Good Component Architecture?
Effective component architecture exhibits these characteristics:
- Modularity: Components have clear, single responsibilities
- Reusability: Components can be used in multiple contexts
- Composability: Components can be combined to create complex UIs
- Maintainability: Easy to understand, modify, and extend
- Testability: Components can be tested in isolation
- Performance: Efficient rendering and minimal re-renders
What Problems Does Good Architecture Solve?
Poor component architecture leads to:
- Props Drilling: Passing data through multiple component layers
- Tight Coupling: Components that are too dependent on each other
- Code Duplication: Similar logic repeated across components
- Hard to Test: Components that are difficult to test in isolation
- Performance Issues: Unnecessary re-renders and poor optimization
- Maintenance Nightmares: Changes in one place break multiple components
How to Choose Between Functional and Class Components? The Modern Approach
What are Functional Components?
Functional components are JavaScript functions that return JSX. They're the modern, preferred way to write React components.
// Simple functional component
function Welcome({ name }) {
return <h1>Hello, {name}</h1>;
}
// Arrow function syntax
const Welcome = ({ name }) => <h1>Hello, {name}</h1>;
// With hooks for state and effects
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
What are Class Components?
Class components are ES6 classes that extend React.Component. They were the traditional way to write React components before hooks.
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
// With state and lifecycle methods
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
componentDidMount() {
document.title = `Count: ${this.state.count}`;
}
componentDidUpdate() {
document.title = `Count: ${this.state.count}`;
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Increment
</button>
</div>
);
}
}
How to Choose Between Functional and Class Components?
Use Functional Components when:
- Building new components (recommended)
- You can use hooks for state and lifecycle needs
- You want simpler, more readable code
- You need better performance (functional components are slightly faster)
- You want easier testing and debugging
Use Class Components when:
- Working with legacy code that can't be easily converted
- You need error boundaries (currently only possible with class components)
- You have complex lifecycle logic that's easier to manage with class methods
- You're working with libraries that haven't been updated for hooks
Why Choose Functional Components?
Functional components offer several advantages:
- Simpler Syntax: Less boilerplate code
- Better Performance: React can optimize functional components better
- Easier Testing: Functions are easier to test than classes
- Modern React: Aligns with React's current direction
- Hooks Integration: Seamless integration with React hooks
- Tree Shaking: Better dead code elimination
How Does Component Composition Work? Building Complex UIs from Simple Parts
What is Component Composition?
Component composition is the practice of building complex user interfaces by combining simpler, reusable components. Instead of creating monolithic components, you break down functionality into smaller, focused pieces.
Composition is what makes React components truly powerful. It allows you to create flexible, reusable components that can be combined in different ways to build various user interfaces.
How to Implement Basic Composition?
// Simple, focused components
function Header({ title }) {
return <header><h1>{title}</h1></header>;
}
function Content({ children }) {
return <main>{children}</main>;
}
function Footer() {
return <footer><p>© 2024 My App</p></footer>;
}
// Composed layout component
function Layout({ title, children }) {
return (
<div className="layout">
<Header title={title} />
<Content>{children}</Content>
<Footer />
</div>
);
}
// Usage
function App() {
return (
<Layout title="My Application">
<p>This is the main content</p>
<p>More content here...</p>
</Layout>
);
}
How to Use the Children Pattern?
The children
prop is a powerful composition pattern that allows components to accept and render child elements:
// Component that uses children
function Card({ title, children }) {
return (
<div className="card">
{title && <h3 className="card-title">{title}</h3>}
<div className="card-content">
{children}
</div>
</div>
);
}
// Usage with different content
function App() {
return (
<div>
<Card title="User Profile">
<p>Name: John Doe</p>
<p>Email: [email protected]</p>
<button>Edit Profile</button>
</Card>
<Card title="Settings">
<form>
<input type="text" placeholder="Username" />
<button type="submit">Save</button>
</form>
</Card>
</div>
);
}
How to Implement Multiple Children Slots?
For more complex composition, you can use multiple named slots:
function Modal({ header, body, footer, isOpen, onClose }) {
if (!isOpen) return null;
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
{header}
</div>
<div className="modal-body">
{body}
</div>
<div className="modal-footer">
{footer}
</div>
</div>
</div>
);
}
// Usage
function App() {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<div>
<button onClick={() => setIsModalOpen(true)}>
Open Modal
</button>
<Modal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
header={<h2>Confirm Action</h2>}
body={<p>Are you sure you want to proceed?</p>}
footer={
<div>
<button onClick={() => setIsModalOpen(false)}>Cancel</button>
<button onClick={() => setIsModalOpen(false)}>Confirm</button>
</div>
}
/>
</div>
);
}
Why Use Component Composition? The Benefits Explained
Why is Composition Better Than Inheritance?
Inheritance approach (not recommended in React):
// This doesn't work well in React
class BaseComponent extends React.Component {
// Shared logic
}
class SpecificComponent extends BaseComponent {
// Specific implementation
}
Composition approach (recommended):
// Reusable components
function Button({ children, variant, onClick }) {
return (
<button
className={`btn btn-${variant}`}
onClick={onClick}
>
{children}
</button>
);
}
function Icon({ name }) {
return <span className={`icon icon-${name}`}></span>;
}
// Composed components
function IconButton({ icon, children, ...props }) {
return (
<Button {...props}>
<Icon name={icon} />
{children}
</Button>
);
}
// Usage
function App() {
return (
<div>
<Button variant="primary">Save</Button>
<IconButton icon="delete" variant="danger">Delete</IconButton>
<IconButton icon="edit" variant="secondary">Edit</IconButton>
</div>
);
}
Why Does Composition Improve Reusability?
Composition makes components more reusable because:
- Single Responsibility: Each component has one clear purpose
- Flexible Combination: Components can be combined in different ways
- Easy Customization: Props allow customization without modification
- Better Testing: Smaller components are easier to test
- Maintainability: Changes to one component don't affect others
Why is Composition Essential for Large Applications?
Large applications benefit from composition because:
- Code Organization: Logical separation of concerns
- Team Collaboration: Different developers can work on different components
- Scalability: Easy to add new features without breaking existing code
- Performance: Only re-render components that actually change
- Debugging: Easier to isolate and fix issues
How to Solve Props Drilling? Advanced Composition Patterns
What is Props Drilling?
Props drilling occurs when you need to pass data through multiple component layers, even when intermediate components don't use that data. This creates tight coupling and makes components harder to maintain.
Props drilling is what happens when your component tree becomes a data pipeline instead of a clean architecture.
How to Identify Props Drilling?
// Props drilling example
function App() {
const [user, setUser] = useState({ name: 'John', role: 'admin' });
return <Header user={user} />;
}
function Header({ user }) {
return <Navigation user={user} />; // Header doesn't use user
}
function Navigation({ user }) {
return <UserMenu user={user} />; // Navigation doesn't use user
}
function UserMenu({ user }) {
return <div>Welcome, {user.name}!</div>; // Only this component uses user
}
How to Solve Props Drilling with Context?
React Context provides a way to share data without passing props through every level:
// Create context
const UserContext = createContext();
// Provider component
function UserProvider({ children }) {
const [user, setUser] = useState({ name: 'John', role: 'admin' });
const value = {
user,
setUser,
isAdmin: user.role === 'admin',
isLoggedIn: !!user
};
return (
<UserContext.Provider value={value}>
{children}
</UserContext.Provider>
);
}
// Custom hook for consuming context
function useUser() {
const context = useContext(UserContext);
if (!context) {
throw new Error('useUser must be used within UserProvider');
}
return context;
}
// Clean component tree
function App() {
return (
<UserProvider>
<Header />
</UserProvider>
);
}
function Header() {
return <Navigation />; // No props needed
}
function Navigation() {
return <UserMenu />; // No props needed
}
function UserMenu() {
const { user, isAdmin } = useUser(); // Direct access to user data
return (
<div>
<span>Welcome, {user.name}!</span>
{isAdmin && <button>Admin Panel</button>}
</div>
);
}
How to Use Render Props Pattern?
Render props is a pattern where a component accepts a function as a prop and calls it with data:
// Data fetching component with render props
function DataFetcher({ url, render, loading, error }) {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(url)
.then(response => response.json())
.then(data => {
setData(data);
setIsLoading(false);
})
.catch(err => {
setError(err);
setIsLoading(false);
});
}, [url]);
if (isLoading) return loading || <div>Loading...</div>;
if (error) return error || <div>Error: {error.message}</div>;
return render(data);
}
// Usage
function App() {
return (
<div>
<DataFetcher
url="/api/users"
loading={<div>Loading users...</div>}
error={<div>Failed to load users</div>}
render={(users) => (
<div>
<h2>Users</h2>
{users.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
)}
/>
<DataFetcher
url="/api/posts"
render={(posts) => (
<div>
<h2>Posts</h2>
{posts.map(post => (
<div key={post.id}>{post.title}</div>
))}
</div>
)}
/>
</div>
);
}
How to Implement Higher-Order Components (HOCs)?
HOCs are functions that take a component and return a new component with additional functionality:
// HOC for authentication
function withAuth(WrappedComponent) {
return function AuthenticatedComponent(props) {
const { user, isLoggedIn } = useUser();
if (!isLoggedIn) {
return <div>Please log in to access this content.</div>;
}
return <WrappedComponent {...props} user={user} />;
};
}
// HOC for loading states
function withLoading(WrappedComponent) {
return function LoadingComponent({ isLoading, ...props }) {
if (isLoading) {
return <div>Loading...</div>;
}
return <WrappedComponent {...props} />;
};
}
// Usage
const AuthenticatedUserProfile = withAuth(UserProfile);
const LoadingUserList = withLoading(UserList);
// Combined HOCs
const AuthenticatedLoadingUserProfile = withAuth(withLoading(UserProfile));
Why Use Advanced Composition Patterns? The Benefits Explained
Why is Context Better Than Props Drilling?
Props drilling problems:
- Tight coupling between components
- Hard to refactor component tree
- Unnecessary re-renders
- Difficult to test components in isolation
Context benefits:
- Loose coupling between components
- Easy to refactor and reorganize
- Components only re-render when context changes
- Easier to test with mock providers
Why Use Render Props?
Render props provide:
- Flexibility: Component behavior can be customized
- Reusability: Same logic, different presentations
- Composition: Easy to combine multiple render props
- Testing: Easy to test logic separately from presentation
Why Use HOCs?
HOCs offer:
- Code Reuse: Share logic across multiple components
- Separation of Concerns: Keep business logic separate from presentation
- Composability: Combine multiple HOCs
- Backward Compatibility: Work with existing components
How to Design Component APIs? Best Practices for Reusable Components
What Makes a Good Component API?
A good component API should be:
- Intuitive: Easy to understand and use
- Flexible: Can be customized for different use cases
- Consistent: Follows established patterns
- Extensible: Easy to add new features
- Performant: Efficient rendering and updates
How to Design Flexible Component APIs?
// Good API design
function Button({
children,
variant = 'primary',
size = 'medium',
disabled = false,
loading = false,
icon,
iconPosition = 'left',
onClick,
className,
...props
}) {
const baseClasses = 'btn';
const variantClasses = `btn-${variant}`;
const sizeClasses = `btn-${size}`;
const stateClasses = disabled ? 'btn-disabled' : '';
const loadingClasses = loading ? 'btn-loading' : '';
const classes = [
baseClasses,
variantClasses,
sizeClasses,
stateClasses,
loadingClasses,
className
].filter(Boolean).join(' ');
const handleClick = (e) => {
if (disabled || loading) return;
onClick?.(e);
};
return (
<button
className={classes}
onClick={handleClick}
disabled={disabled}
{...props}
>
{loading && <Spinner size="small" />}
{icon && iconPosition === 'left' && <Icon name={icon} />}
{children}
{icon && iconPosition === 'right' && <Icon name={icon} />}
</button>
);
}
// Usage examples
function App() {
return (
<div>
<Button>Default Button</Button>
<Button variant="secondary" size="large">Large Secondary</Button>
<Button icon="save" iconPosition="right">Save</Button>
<Button loading disabled>Loading...</Button>
<Button
variant="danger"
onClick={() => console.log('Delete')}
className="custom-class"
>
Delete
</Button>
</div>
);
}
How to Handle Component Variants?
// Variant-based component design
function Card({
variant = 'default',
children,
className,
...props
}) {
const variants = {
default: 'card-default',
elevated: 'card-elevated',
outlined: 'card-outlined',
filled: 'card-filled'
};
const classes = [
'card',
variants[variant],
className
].filter(Boolean).join(' ');
return (
<div className={classes} {...props}>
{children}
</div>
);
}
// Compound component pattern
function CardHeader({ children, className, ...props }) {
return (
<div className={`card-header ${className || ''}`} {...props}>
{children}
</div>
);
}
function CardBody({ children, className, ...props }) {
return (
<div className={`card-body ${className || ''}`} {...props}>
{children}
</div>
);
}
function CardFooter({ children, className, ...props }) {
return (
<div className={`card-footer ${className || ''}`} {...props}>
{children}
</div>
);
}
// Attach sub-components
Card.Header = CardHeader;
Card.Body = CardBody;
Card.Footer = CardFooter;
// Usage
function App() {
return (
<Card variant="elevated">
<Card.Header>
<h3>Card Title</h3>
</Card.Header>
<Card.Body>
<p>Card content goes here</p>
</Card.Body>
<Card.Footer>
<Button>Action</Button>
</Card.Footer>
</Card>
);
}
Component Architecture Best Practices
What Are Component Architecture Best Practices?
- Single Responsibility: Each component should have one clear purpose
- Composition Over Inheritance: Use composition patterns instead of inheritance
- Props Interface Design: Design clear, consistent prop interfaces
- Performance Optimization: Use React.memo, useMemo, and useCallback appropriately
- Error Boundaries: Implement error boundaries for robust error handling
- Testing Strategy: Design components to be easily testable
How to Implement Performance Optimization?
// Memoized component
const ExpensiveComponent = React.memo(function ExpensiveComponent({ data, onUpdate }) {
const processedData = useMemo(() => {
return data.map(item => ({
...item,
processed: expensiveCalculation(item)
}));
}, [data]);
const handleUpdate = useCallback((id, updates) => {
onUpdate(id, updates);
}, [onUpdate]);
return (
<div>
{processedData.map(item => (
<Item
key={item.id}
data={item}
onUpdate={handleUpdate}
/>
))}
</div>
);
});
// Error boundary
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="error-boundary">
<h2>Something went wrong.</h2>
<details>
{this.state.error && this.state.error.toString()}
</details>
</div>
);
}
return this.props.children;
}
}
Why Follow Component Architecture Best Practices?
Following best practices ensures:
- Maintainability: Easy to understand and modify
- Scalability: Can grow with your application
- Performance: Efficient rendering and updates
- Reliability: Robust error handling and testing
- Developer Experience: Better tooling and debugging
- Team Collaboration: Consistent patterns across the team
Summary: Building Scalable React Applications
What Have We Learned?
In this chapter, we've explored the fundamental principles of React component architecture:
- Component Architecture: The systematic approach to designing maintainable, scalable React applications
- Functional vs Class Components: Modern React development with functional components and hooks
- Component Composition: Building complex UIs from simple, reusable components
- Advanced Patterns: Context API, render props, and HOCs for solving complex problems
- API Design: Creating intuitive, flexible, and performant component interfaces
- Best Practices: Performance optimization, error handling, and testing strategies
How to Apply These Concepts?
- Start Simple: Begin with basic composition patterns
- Identify Problems: Look for props drilling and tight coupling
- Choose Solutions: Use Context for global state, render props for flexibility
- Design APIs: Create consistent, intuitive component interfaces
- Optimize Performance: Use React.memo, useMemo, and useCallback appropriately
- Handle Errors: Implement error boundaries for robust applications
Why This Matters for Your React Journey?
Understanding component architecture is crucial because:
- Scalability: Your applications can grow without becoming unmaintainable
- Team Collaboration: Clear patterns make it easier for teams to work together
- Performance: Well-architected components are more efficient
- User Experience: Better architecture leads to better user experiences
- Developer Experience: Clean architecture makes development more enjoyable
Next Steps
Now that you understand component architecture, you're ready to explore:
- State Management: How to manage complex application state
- Event Handling: How to handle user interactions effectively
- Advanced Hooks: How to use React hooks for complex scenarios
- Performance Optimization: How to build fast, responsive applications
Remember: Good architecture is not about following rules blindly, but about making informed decisions that serve your application's needs and your team's goals.
// Custom hook
function useUser() {
const context = useContext(UserContext);
if (!context) {
throw new Error('useUser must be used within a UserProvider');
}
return context;
}
// App component
function App() {
return (
<UserProvider>
<Parent />
</UserProvider>
);
}
// Components can now access user directly
function Grandchild() {
const { user } = useUser();
return <p>Hello, {user.name}!</p>;
}
Solution 2: Component Composition
// Instead of passing props down, compose components
function App() {
const [user, setUser] = useState({ name: 'John', role: 'admin' });
return (
<UserProvider user={user}>
<Layout>
<Header />
<MainContent />
<Sidebar />
</Layout>
</UserProvider>
);
}
function UserProvider({ user, children }) {
return (
<UserContext.Provider value={user}>
{children}
</UserContext.Provider>
);
}
Component Design Patterns
Container and Presentational Components
Separate components into containers (logic) and presentational (UI) components.
// Presentational component (UI only)
function UserList({ users, onUserSelect }) {
return (
<ul>
{users.map(user => (
<li key={user.id} onClick={() => onUserSelect(user)}>
{user.name}
</li>
))}
</ul>
);
}
// Container component (logic)
function UserListContainer() {
const [users, setUsers] = useState([]);
const [selectedUser, setSelectedUser] = useState(null);
useEffect(() => {
// Fetch users from API
fetchUsers().then(setUsers);
}, []);
const handleUserSelect = (user) => {
setSelectedUser(user);
};
return (
<div>
<UserList users={users} onUserSelect={handleUserSelect} />
{selectedUser && <UserDetails user={selectedUser} />}
</div>
);
}
Custom Hooks Pattern
Extract logic into custom hooks for reusability.
// Custom hook
function useUsers() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const fetchUsers = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch('/api/users');
const data = await response.json();
setUsers(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchUsers();
}, []);
return { users, loading, error, refetch: fetchUsers };
}
// Component using the hook
function UserList() {
const { users, loading, error } = useUsers();
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Higher-Order Components (HOCs)
HOCs are functions that take a component and return a new component with additional functionality.
// HOC for authentication
function withAuth(WrappedComponent) {
return function AuthenticatedComponent(props) {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [user, setUser] = useState(null);
useEffect(() => {
// Check authentication status
checkAuth().then(({ authenticated, userData }) => {
setIsAuthenticated(authenticated);
setUser(userData);
});
}, []);
if (!isAuthenticated) {
return <LoginForm />;
}
return <WrappedComponent {...props} user={user} />;
};
}
// Usage
const ProtectedProfile = withAuth(Profile);
function App() {
return <ProtectedProfile />;
}
HOC with Additional Props
// HOC that adds loading state
function withLoading(WrappedComponent) {
return function LoadingComponent({ isLoading, ...props }) {
if (isLoading) {
return <div>Loading...</div>;
}
return <WrappedComponent {...props} />;
};
}
// Usage
const UserListWithLoading = withLoading(UserList);
function App() {
const [loading, setLoading] = useState(true);
const [users, setUsers] = useState([]);
useEffect(() => {
fetchUsers().then(data => {
setUsers(data);
setLoading(false);
});
}, []);
return <UserListWithLoading isLoading={loading} users={users} />;
}
Render Props Pattern
Render props is a pattern where a component accepts a function as a prop and calls it to render content.
// Component with render prop
function DataFetcher({ url, render }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(url)
.then(response => response.json())
.then(data => {
setData(data);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, [url]);
return render({ data, loading, error });
}
// Usage
function App() {
return (
<DataFetcher
url="/api/users"
render={({ data, loading, error }) => {
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}}
/>
);
}
Children as Function Pattern
// Component that uses children as function
function Toggle({ children }) {
const [isOn, setIsOn] = useState(false);
const toggle = () => setIsOn(!isOn);
return children({ isOn, toggle });
}
// Usage
function App() {
return (
<Toggle>
{({ isOn, toggle }) => (
<div>
<button onClick={toggle}>
{isOn ? 'ON' : 'OFF'}
</button>
<p>The switch is {isOn ? 'on' : 'off'}</p>
</div>
)}
</Toggle>
);
}
Compound Components
Compound components are components that work together to form a complete UI.
// Compound component example
function Tabs({ children, defaultTab }) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<div className="tabs">
{React.Children.map(children, child => {
if (child.type === TabsList) {
return React.cloneElement(child, { activeTab, setActiveTab });
}
if (child.type === TabsContent) {
return React.cloneElement(child, { activeTab });
}
return child;
})}
</div>
);
}
function TabsList({ children, activeTab, setActiveTab }) {
return (
<div className="tabs-list">
{React.Children.map(children, child => {
return React.cloneElement(child, {
isActive: child.props.value === activeTab,
onClick: () => setActiveTab(child.props.value)
});
})}
</div>
);
}
function TabsTrigger({ children, value, isActive, onClick }) {
return (
<button
className={`tab-trigger ${isActive ? 'active' : ''}`}
onClick={onClick}
>
{children}
</button>
);
}
function TabsContent({ children, activeTab }) {
return (
<div className="tabs-content">
{React.Children.map(children, child => {
if (child.props.value === activeTab) {
return child;
}
return null;
})}
</div>
);
}
function TabsPanel({ children, value }) {
return <div className="tab-panel">{children}</div>;
}
// Usage
function App() {
return (
<Tabs defaultTab="tab1">
<TabsList>
<TabsTrigger value="tab1">Tab 1</TabsTrigger>
<TabsTrigger value="tab2">Tab 2</TabsTrigger>
<TabsTrigger value="tab3">Tab 3</TabsTrigger>
</TabsList>
<TabsContent>
<TabsPanel value="tab1">
<h3>Content for Tab 1</h3>
<p>This is the content for the first tab.</p>
</TabsPanel>
<TabsPanel value="tab2">
<h3>Content for Tab 2</h3>
<p>This is the content for the second tab.</p>
</TabsPanel>
<TabsPanel value="tab3">
<h3>Content for Tab 3</h3>
<p>This is the content for the third tab.</p>
</TabsPanel>
</TabsContent>
</Tabs>
);
}
Component Lifecycle and Optimization
React.memo for Performance
// Memoized component
const ExpensiveComponent = React.memo(function ExpensiveComponent({ data }) {
// Expensive computation
const processedData = useMemo(() => {
return data.map(item => ({
...item,
processed: true
}));
}, [data]);
return (
<div>
{processedData.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
});
// Usage
function App() {
const [data, setData] = useState([]);
const [otherState, setOtherState] = useState('');
return (
<div>
<input
value={otherState}
onChange={(e) => setOtherState(e.target.value)}
/>
<ExpensiveComponent data={data} />
</div>
);
}
useMemo and useCallback
function ProductList({ products, filter, onProductSelect }) {
// Memoize filtered products
const filteredProducts = useMemo(() => {
return products.filter(product =>
product.name.toLowerCase().includes(filter.toLowerCase())
);
}, [products, filter]);
// Memoize callback function
const handleProductSelect = useCallback((product) => {
onProductSelect(product);
}, [onProductSelect]);
return (
<div>
{filteredProducts.map(product => (
<ProductItem
key={product.id}
product={product}
onSelect={handleProductSelect}
/>
))}
</div>
);
}
Practice Project: Component Library
Let's build a reusable component library to practice the patterns we've learned:
// Button component with variants
function Button({
children,
variant = 'primary',
size = 'medium',
disabled = false,
onClick,
...props
}) {
const baseClasses = 'btn';
const variantClasses = {
primary: 'btn-primary',
secondary: 'btn-secondary',
danger: 'btn-danger'
};
const sizeClasses = {
small: 'btn-sm',
medium: 'btn-md',
large: 'btn-lg'
};
const className = [
baseClasses,
variantClasses[variant],
sizeClasses[size],
disabled ? 'btn-disabled' : ''
].filter(Boolean).join(' ');
return (
<button
className={className}
disabled={disabled}
onClick={onClick}
{...props}
>
{children}
</button>
);
}
// Input component with validation
function Input({
label,
error,
required = false,
...props
}) {
return (
<div className="input-group">
{label && (
<label className="input-label">
{label}
{required && <span className="required">*</span>}
</label>
)}
<input
className={`input ${error ? 'input-error' : ''}`}
{...props}
/>
{error && <span className="error-message">{error}</span>}
</div>
);
}
// Form component using compound pattern
function Form({ children, onSubmit, ...props }) {
const [errors, setErrors] = useState({});
const [values, setValues] = useState({});
const handleSubmit = (e) => {
e.preventDefault();
onSubmit(values, errors);
};
const contextValue = {
values,
setValues,
errors,
setErrors
};
return (
<FormContext.Provider value={contextValue}>
<form onSubmit={handleSubmit} {...props}>
{children}
</form>
</FormContext.Provider>
);
}
// Usage example
function ContactForm() {
const handleSubmit = (values, errors) => {
if (Object.keys(errors).length === 0) {
console.log('Form submitted:', values);
}
};
return (
<Form onSubmit={handleSubmit}>
<Input
name="name"
label="Name"
required
placeholder="Enter your name"
/>
<Input
name="email"
label="Email"
type="email"
required
placeholder="Enter your email"
/>
<div className="form-actions">
<Button type="submit">Submit</Button>
<Button variant="secondary" type="button">Cancel</Button>
</div>
</Form>
);
}
Summary
In this chapter, we explored advanced component architecture patterns:
- Component Types: Functional vs Class components
- Composition: Building complex UIs from simple components
- Props Drilling: Problems and solutions using Context API
- Design Patterns: Container/Presentational, Custom Hooks
- HOCs: Higher-order components for cross-cutting concerns
- Render Props: Flexible component composition
- Compound Components: Components that work together
- Performance: Optimization with React.memo, useMemo, useCallback