Component Composition
Learn advanced component composition patterns to build flexible, reusable, and maintainable React applications.
Learning Objectives
- Understand composition vs inheritance in React
- Master the children prop and containment patterns
- Learn higher-order components (HOCs) and their use cases
- Implement render props pattern for flexible components
- Build compound components for complex UI patterns
- Apply composition patterns to real-world scenarios
Component Composition
Component composition is one of React's most powerful features. Instead of using class inheritance, React uses composition to build complex UIs from simple, reusable components. This lesson covers advanced composition patterns that will make your components more flexible and maintainable.
Composition vs Inheritance
React favors composition over inheritance. Instead of creating complex class hierarchies, you build functionality by combining simple components.
The Problem with Inheritance
// Traditional OOP approach (not recommended in React) class BaseButton extends Component { render() { return <button className="btn">{this.props.children}</button>; } } class PrimaryButton extends BaseButton { render() { return <button className="btn btn-primary">{this.props.children}</button>; } } class DangerButton extends BaseButton { render() { return <button className="btn btn-danger">{this.props.children}</button>; } }
The Composition Solution
// React composition approach (recommended) function Button({ variant = 'default', children, ...props }) { return ( <button className={`btn btn-${variant}`} {...props} > {children} </button> ); } // Usage - much more flexible <Button variant="primary">Primary Action</Button> <Button variant="danger">Delete Item</Button> <Button variant="success" onClick={handleSave}>Save Changes</Button>
The Children Prop Pattern
The children
prop is the foundation of composition in React. It allows components to be generic containers.
Basic Containment
function Card({ title, children }) { return ( <div className="card"> {title && ( <div className="card-header"> <h3>{title}</h3> </div> )} <div className="card-body"> {children} </div> </div> ); } // Usage - Card can contain any content <Card title="User Profile"> <img src="avatar.jpg" alt="User" /> <h4>John Doe</h4> <p>Software Engineer</p> <button>Edit Profile</button> </Card> <Card title="Statistics"> <div className="stats-grid"> <div className="stat"> <span className="number">1,234</span> <span className="label">Users</span> </div> <div className="stat"> <span className="number">5,678</span> <span className="label">Posts</span> </div> </div> </Card>
Layout Components
function Container({ size = 'md', children }) { const sizes = { sm: 'max-w-4xl', md: 'max-w-6xl', lg: 'max-w-7xl' }; return ( <div className={`container mx-auto px-4 ${sizes[size]}`}> {children} </div> ); } function Flex({ direction = 'row', gap = '4', children, ...props }) { return ( <div className={`flex flex-${direction} gap-${gap}`} {...props} > {children} </div> ); } function Grid({ cols = '1', gap = '4', children }) { return ( <div className={`grid grid-cols-${cols} gap-${gap}`}> {children} </div> ); } // Building layouts with composition function HomePage() { return ( <Container size="lg"> <Flex direction="col" gap="8"> <header> <h1>Welcome to Our App</h1> </header> <Flex gap="6"> <main className="flex-1"> <Grid cols="2" gap="6"> <Card title="Recent Posts"> <PostList /> </Card> <Card title="Popular Topics"> <TopicList /> </Card> </Grid> </main> <aside className="w-80"> <Card title="User Stats"> <UserStats /> </Card> </aside> </Flex> </Flex> </Container> ); }
Specialized Composition
function Dialog({ children, isOpen, onClose }) { if (!isOpen) return null; return ( <div className="dialog-overlay" onClick={onClose}> <div className="dialog-content" onClick={(e) => e.stopPropagation()} > {children} </div> </div> ); } function DialogHeader({ children }) { return ( <div className="dialog-header"> {children} </div> ); } function DialogBody({ children }) { return ( <div className="dialog-body"> {children} </div> ); } function DialogFooter({ children }) { return ( <div className="dialog-footer"> {children} </div> ); } // Usage - flexible dialog composition <Dialog isOpen={showConfirm} onClose={() => setShowConfirm(false)}> <DialogHeader> <h2>Confirm Delete</h2> </DialogHeader> <DialogBody> <p>Are you sure you want to delete this item? This action cannot be undone.</p> </DialogBody> <DialogFooter> <button onClick={() => setShowConfirm(false)}>Cancel</button> <button onClick={handleDelete} className="btn-danger">Delete</button> </DialogFooter> </Dialog>
Higher-Order Components (HOCs)
Higher-Order Components are functions that take a component and return a new component with additional functionality.
Basic HOC Pattern
// HOC that adds loading state function withLoading(WrappedComponent) { return function WithLoadingComponent(props) { if (props.isLoading) { return ( <div className="loading-container"> <div className="spinner"></div> <p>Loading...</p> </div> ); } return <WrappedComponent {...props} />; }; } // Original components function UserList({ users }) { return ( <div> {users.map(user => ( <div key={user.id}>{user.name}</div> ))} </div> ); } function ProductList({ products }) { return ( <div> {products.map(product => ( <div key={product.id}>{product.name}</div> ))} </div> ); } // Enhanced components with loading const UserListWithLoading = withLoading(UserList); const ProductListWithLoading = withLoading(ProductList); // Usage <UserListWithLoading users={users} isLoading={loadingUsers} /> <ProductListWithLoading products={products} isLoading={loadingProducts} />
Authentication HOC
function withAuth(WrappedComponent) { return function WithAuthComponent(props) { const { user, isAuthenticated } = useAuth(); // Custom hook if (!isAuthenticated) { return ( <div className="auth-required"> <h2>Authentication Required</h2> <p>Please log in to access this content.</p> <LoginButton /> </div> ); } return <WrappedComponent {...props} user={user} />; }; } // Protect components with authentication const ProtectedUserProfile = withAuth(UserProfile); const ProtectedSettings = withAuth(Settings); const ProtectedDashboard = withAuth(Dashboard); // Usage <ProtectedUserProfile userId={123} /> <ProtectedSettings /> <ProtectedDashboard />
Data Fetching HOC
function withData(url, propName = 'data') { return function(WrappedComponent) { return function WithDataComponent(props) { 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(error => { setError(error.message); setLoading(false); }); }, [url]); if (loading) return <div>Loading...</div>; if (error) return <div>Error: {error}</div>; const enhancedProps = { ...props, [propName]: data, isLoading: loading, error: error }; return <WrappedComponent {...enhancedProps} />; }; }; } // Usage const UserListWithData = withData('/api/users', 'users')(UserList); const PostListWithData = withData('/api/posts', 'posts')(PostList); // Components automatically get data <UserListWithData /> <PostListWithData />
Combining Multiple HOCs
import { compose } from 'redux'; // or create your own compose function function compose(...fns) { return (value) => fns.reduceRight((acc, fn) => fn(acc), value); } // Combine multiple HOCs const EnhancedUserList = compose( withAuth, withLoading, withData('/api/users', 'users') )(UserList); // Or manually const EnhancedUserList = withAuth( withLoading( withData('/api/users', 'users')(UserList) ) ); // Usage - component has all enhancements <EnhancedUserList />
Render Props Pattern
Render props is a pattern where a component takes a function as a prop and calls it to determine what to render.
Basic Render Props
function DataProvider({ url, children }) { 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(error => { setError(error.message); setLoading(false); }); }, [url]); // Call children as a function with state return children({ data, loading, error }); } // Usage <DataProvider url="/api/users"> {({ data, loading, error }) => { if (loading) return <LoadingSpinner />; if (error) return <ErrorMessage message={error} />; return ( <UserList users={data} /> ); }} </DataProvider> <DataProvider url="/api/posts"> {({ data, loading, error }) => { if (loading) return <div>Loading posts...</div>; if (error) return <div>Failed to load posts</div>; return ( <div> <h2>Latest Posts</h2> <PostGrid posts={data} /> </div> ); }} </DataProvider>
Mouse Position Tracker
function MouseTracker({ children }) { const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }); useEffect(() => { const handleMouseMove = (event) => { setMousePosition({ x: event.clientX, y: event.clientY }); }; document.addEventListener('mousemove', handleMouseMove); return () => { document.removeEventListener('mousemove', handleMouseMove); }; }, []); return children(mousePosition); } // Usage <MouseTracker> {({ x, y }) => ( <div> <h2>Mouse Position</h2> <p>X: {x}, Y: {y}</p> <div style={{ position: 'absolute', left: x - 10, top: y - 10, width: 20, height: 20, backgroundColor: 'red', borderRadius: '50%', pointerEvents: 'none' }} /> </div> )} </MouseTracker>
Form State Manager
function FormProvider({ initialValues = {}, onSubmit, children }) { const [values, setValues] = useState(initialValues); const [errors, setErrors] = useState({}); const [touched, setTouched] = useState({}); const setValue = (name, value) => { setValues(prev => ({ ...prev, [name]: value })); // Clear error when user starts typing if (errors[name]) { setErrors(prev => ({ ...prev, [name]: undefined })); } }; const setTouched = (name) => { setTouched(prev => ({ ...prev, [name]: true })); }; const validate = () => { const newErrors = {}; // Simple validation rules Object.keys(values).forEach(key => { if (!values[key] || values[key].trim() === '') { newErrors[key] = 'This field is required'; } }); setErrors(newErrors); return Object.keys(newErrors).length === 0; }; const handleSubmit = (e) => { e.preventDefault(); if (validate()) { onSubmit(values); } }; return children({ values, errors, touched, setValue, setTouched, handleSubmit, isValid: Object.keys(errors).length === 0 }); } // Usage <FormProvider initialValues={{ name: '', email: '' }} onSubmit={(data) => console.log('Form submitted:', data)} > {({ values, errors, setValue, setTouched, handleSubmit, isValid }) => ( <form onSubmit={handleSubmit}> <div className="form-field"> <label>Name</label> <input type="text" value={values.name || ''} onChange={(e) => setValue('name', e.target.value)} onBlur={() => setTouched('name')} /> {errors.name && <span className="error">{errors.name}</span>} </div> <div className="form-field"> <label>Email</label> <input type="email" value={values.email || ''} onChange={(e) => setValue('email', e.target.value)} onBlur={() => setTouched('email')} /> {errors.email && <span className="error">{errors.email}</span>} </div> <button type="submit" disabled={!isValid}> Submit </button> </form> )} </FormProvider>
Compound Components
Compound components work together to form a complete UI pattern. They share state implicitly and provide a clean API.
Tab Component System
import { createContext, useContext, useState } from 'react'; // Create context for tab state const TabContext = createContext(); function Tabs({ children, defaultTab }) { const [activeTab, setActiveTab] = useState(defaultTab); return ( <TabContext.Provider value={{ activeTab, setActiveTab }}> <div className="tabs"> {children} </div> </TabContext.Provider> ); } function TabList({ children }) { return ( <div className="tab-list" role="tablist"> {children} </div> ); } function Tab({ value, children }) { const { activeTab, setActiveTab } = useContext(TabContext); const isActive = activeTab === value; return ( <button className={`tab ${isActive ? 'tab-active' : ''}`} onClick={() => setActiveTab(value)} role="tab" aria-selected={isActive} > {children} </button> ); } function TabPanels({ children }) { return ( <div className="tab-panels"> {children} </div> ); } function TabPanel({ value, children }) { const { activeTab } = useContext(TabContext); if (value !== activeTab) return null; return ( <div className="tab-panel" role="tabpanel"> {children} </div> ); } // Usage - clean, declarative API <Tabs defaultTab="profile"> <TabList> <Tab value="profile">Profile</Tab> <Tab value="settings">Settings</Tab> <Tab value="billing">Billing</Tab> </TabList> <TabPanels> <TabPanel value="profile"> <UserProfile /> </TabPanel> <TabPanel value="settings"> <UserSettings /> </TabPanel> <TabPanel value="billing"> <BillingInfo /> </TabPanel> </TabPanels> </Tabs>
Accordion Component System
const AccordionContext = createContext(); function Accordion({ children, allowMultiple = false }) { const [openItems, setOpenItems] = useState(new Set()); const toggleItem = (value) => { setOpenItems(prev => { const newSet = new Set(prev); if (newSet.has(value)) { newSet.delete(value); } else { if (!allowMultiple) { newSet.clear(); } newSet.add(value); } return newSet; }); }; return ( <AccordionContext.Provider value={{ openItems, toggleItem }}> <div className="accordion"> {children} </div> </AccordionContext.Provider> ); } function AccordionItem({ value, children }) { return ( <div className="accordion-item"> {children} </div> ); } function AccordionTrigger({ value, children }) { const { openItems, toggleItem } = useContext(AccordionContext); const isOpen = openItems.has(value); return ( <button className={`accordion-trigger ${isOpen ? 'accordion-trigger-open' : ''}`} onClick={() => toggleItem(value)} aria-expanded={isOpen} > {children} <span className="accordion-icon"> {isOpen ? '−' : '+'} </span> </button> ); } function AccordionContent({ value, children }) { const { openItems } = useContext(AccordionContext); const isOpen = openItems.has(value); return ( <div className={`accordion-content ${isOpen ? 'accordion-content-open' : ''}`}> {children} </div> ); } // Usage <Accordion allowMultiple={true}> <AccordionItem value="faq1"> <AccordionTrigger value="faq1"> What is React? </AccordionTrigger> <AccordionContent value="faq1"> React is a JavaScript library for building user interfaces. </AccordionContent> </AccordionItem> <AccordionItem value="faq2"> <AccordionTrigger value="faq2"> How do I get started? </AccordionTrigger> <AccordionContent value="faq2"> You can start by creating a new React app with Create React App. </AccordionContent> </AccordionItem> </Accordion>
Dropdown Menu System
const DropdownContext = createContext(); function Dropdown({ children }) { const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null); useEffect(() => { const handleClickOutside = (event) => { if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { setIsOpen(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); return ( <DropdownContext.Provider value={{ isOpen, setIsOpen }}> <div className="dropdown" ref={dropdownRef}> {children} </div> </DropdownContext.Provider> ); } function DropdownTrigger({ children }) { const { isOpen, setIsOpen } = useContext(DropdownContext); return ( <button className="dropdown-trigger" onClick={() => setIsOpen(!isOpen)} aria-expanded={isOpen} > {children} </button> ); } function DropdownMenu({ children }) { const { isOpen } = useContext(DropdownContext); if (!isOpen) return null; return ( <div className="dropdown-menu"> {children} </div> ); } function DropdownItem({ children, onClick }) { const { setIsOpen } = useContext(DropdownContext); return ( <button className="dropdown-item" onClick={() => { onClick?.(); setIsOpen(false); }} > {children} </button> ); } // Usage <Dropdown> <DropdownTrigger> User Menu <span>▼</span> </DropdownTrigger> <DropdownMenu> <DropdownItem onClick={() => navigate('/profile')}> View Profile </DropdownItem> <DropdownItem onClick={() => navigate('/settings')}> Settings </DropdownItem> <DropdownItem onClick={handleLogout}> Sign Out </DropdownItem> </DropdownMenu> </Dropdown>
Real-World Application
Complete Dashboard Layout System
// Layout components with composition function Dashboard({ children }) { return ( <div className="dashboard"> {children} </div> ); } function DashboardHeader({ children }) { return ( <header className="dashboard-header"> {children} </header> ); } function DashboardSidebar({ children }) { return ( <aside className="dashboard-sidebar"> {children} </aside> ); } function DashboardMain({ children }) { return ( <main className="dashboard-main"> {children} </main> ); } function DashboardFooter({ children }) { return ( <footer className="dashboard-footer"> {children} </footer> ); } // Widget components function Widget({ title, actions, children }) { return ( <div className="widget"> <div className="widget-header"> <h3 className="widget-title">{title}</h3> {actions && ( <div className="widget-actions"> {actions} </div> )} </div> <div className="widget-content"> {children} </div> </div> ); } // Usage - flexible dashboard composition function App() { return ( <Dashboard> <DashboardHeader> <h1>My Dashboard</h1> <UserMenu /> </DashboardHeader> <DashboardSidebar> <Navigation /> </DashboardSidebar> <DashboardMain> <Grid cols="2" gap="6"> <Widget title="Sales Overview" actions={<RefreshButton />} > <SalesChart /> </Widget> <Widget title="Recent Orders"> <OrdersList /> </Widget> <Widget title="User Activity" actions={ <Dropdown> <DropdownTrigger>Options</DropdownTrigger> <DropdownMenu> <DropdownItem>Export Data</DropdownItem> <DropdownItem>View Details</DropdownItem> </DropdownMenu> </Dropdown> } > <ActivityFeed /> </Widget> <Widget title="Performance"> <PerformanceMetrics /> </Widget> </Grid> </DashboardMain> <DashboardFooter> <p>© 2024 My Company</p> </DashboardFooter> </Dashboard> ); }
Best Practices
1. Prefer Composition Over Complex Props
❌ Bad - Complex prop interface:
function Modal({ isOpen, title, content, showHeader, showFooter, primaryAction, secondaryAction, headerContent, footerContent }) { return ( <div className="modal"> {showHeader && ( <div className="modal-header"> {headerContent || <h2>{title}</h2>} </div> )} <div className="modal-body"> {content} </div> {showFooter && ( <div className="modal-footer"> {footerContent || ( <> {secondaryAction} {primaryAction} </> )} </div> )} </div> ); }
✅ Good - Composition approach:
function Modal({ isOpen, children, onClose }) { if (!isOpen) return null; return ( <div className="modal-overlay" onClick={onClose}> <div className="modal-content" onClick={(e) => e.stopPropagation()}> {children} </div> </div> ); } // Usage - much more flexible <Modal isOpen={showModal} onClose={closeModal}> <ModalHeader> <h2>Custom Title</h2> <CloseButton onClick={closeModal} /> </ModalHeader> <ModalBody> <p>Any content can go here</p> <CustomForm /> </ModalBody> <ModalFooter> <Button variant="secondary" onClick={closeModal}>Cancel</Button> <Button variant="primary" onClick={handleSave}>Save</Button> </ModalFooter> </Modal>
2. Use Context for Compound Components
✅ Good - Context provides clean API:
// Components know about each other through context const FormContext = createContext(); function Form({ children, onSubmit }) { const [values, setValues] = useState({}); return ( <FormContext.Provider value={{ values, setValues }}> <form onSubmit={onSubmit}> {children} </form> </FormContext.Provider> ); } function Field({ name, children }) { const { values, setValues } = useContext(FormContext); return ( <div className="field"> {React.cloneElement(children, { value: values[name] || '', onChange: (e) => setValues(prev => ({ ...prev, [name]: e.target.value })) })} </div> ); } // Usage <Form onSubmit={handleSubmit}> <Field name="username"> <input type="text" placeholder="Username" /> </Field> <Field name="password"> <input type="password" placeholder="Password" /> </Field> </Form>
3. Keep HOCs Simple and Focused
✅ Good - Single responsibility:
// Each HOC has one job const withAuth = (Component) => (props) => { const { isAuthenticated } = useAuth(); return isAuthenticated ? <Component {...props} /> : <LoginPrompt />; }; const withLoading = (Component) => (props) => { return props.loading ? <LoadingSpinner /> : <Component {...props} />; }; const withErrorHandling = (Component) => (props) => { return props.error ? <ErrorMessage error={props.error} /> : <Component {...props} />; }; // Compose them together const EnhancedComponent = withAuth( withLoading( withErrorHandling(MyComponent) ) );
Summary
Component composition is a powerful pattern that makes React applications more maintainable and flexible:
✅ Use children prop for containment and layout
✅ Apply HOCs for cross-cutting concerns
✅ Leverage render props for flexible data sharing
✅ Build compound components for complex UI patterns
✅ Prefer composition over inheritance
✅ Keep each pattern focused and simple
What's Next?
In the next lesson, we'll dive into React State - how to add interactivity to your components with the useState hook. You'll learn:
- What state is and why it's important
- Using useState hook effectively
- State updates and immutability
- Managing complex state
- State vs props
State is what makes your components come alive with interactivity!