Back to Tutorial
Components and Props
Theory Lesson
35 min

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>&copy; 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!