Component Composition and Patterns
Learn advanced component composition techniques, children props, render props, and compound components to build flexible, reusable React components.
Topics Covered:
Prerequisites:
- Understanding Props
- Managing State with useState
- Conditional Rendering and Lists
Video Tutorial
Overview
Component composition is one of React's most powerful features. Instead of building monolithic components, you compose smaller, focused components together. This tutorial covers various composition patterns including children, render props, compound components, and higher-order components to create flexible, reusable component APIs.
Understanding Component Composition
Component composition means building complex UIs by combining simpler components. It's the foundation of React's component model. Benefits: • Reusability - Use components in different contexts • Flexibility - Combine components in various ways • Maintainability - Smaller, focused components • Testability - Test components in isolation • Readability - Clear component structure Composition vs Inheritance: • React favors composition over inheritance • Use composition to share behavior • Components can contain other components • Props allow flexible composition
// Simple composition
function Button({ children, onClick }) {
return <button onClick={onClick}>{children}</button>;
}
function App() {
return (
<div>
<Button onClick={() => console.log('Save')}>
Save
</Button>
<Button onClick={() => console.log('Cancel')}>
Cancel
</Button>
</div>
);
}
// Composing multiple components
function Card({ title, children }) {
return (
<div className="card">
<h2>{title}</h2>
<div className="card-content">{children}</div>
</div>
);
}
function UserProfile({ user }) {
return (
<Card title="User Profile">
<p>Name: {user.name}</p>
<p>Email: {user.email}</p>
</Card>
);
}
// Nested composition
function Layout({ header, sidebar, main, footer }) {
return (
<div className="layout">
<header>{header}</header>
<div className="body">
<aside>{sidebar}</aside>
<main>{main}</main>
</div>
<footer>{footer}</footer>
</div>
);
}
function App() {
return (
<Layout
header={<Header />}
sidebar={<Sidebar />}
main={<MainContent />}
footer={<Footer />}
/>
);
}Composition means building complex components from simpler ones. Use props to pass components and data. This creates flexible, reusable component structures.
Using the Children Prop
The children prop is a special prop that allows you to pass components as data to other components. It's one of the most powerful composition patterns. What is Children: • Special prop that contains content between component tags • Can be any valid React node • Can be a single element, array, or even a function • Makes components more flexible and reusable Use Cases: • Wrapper components (Card, Modal, Layout) • Container components • Higher-order components • Flexible component APIs
// Basic children usage
function Container({ children }) {
return <div className="container">{children}</div>;
}
function App() {
return (
<Container>
<h1>Title</h1>
<p>Content</p>
</Container>
);
}
// Children can be anything
function Alert({ type, children }) {
return (
<div className={`alert alert-${type}`}>
{children}
</div>
);
}
function App() {
return (
<Alert type="warning">
<strong>Warning!</strong> This is important.
</Alert>
);
}
// Multiple children
function ButtonGroup({ children }) {
return <div className="button-group">{children}</div>;
}
function App() {
return (
<ButtonGroup>
<button>Save</button>
<button>Cancel</button>
<button>Delete</button>
</ButtonGroup>
);
}
// Children as function (render prop pattern)
function DataFetcher({ url, children }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(data => {
setData(data);
setLoading(false);
});
}, [url]);
return children({ data, loading });
}
function App() {
return (
<DataFetcher url="/api/users">
{({ data, loading }) => {
if (loading) return <div>Loading...</div>;
return <UserList users={data} />;
}}
</DataFetcher>
);
}
// Using React.Children utilities
function ButtonList({ children }) {
return (
<div>
{React.Children.map(children, (child, index) => (
<div key={index} className="button-wrapper">
{child}
</div>
))}
</div>
);
}The children prop allows flexible composition. You can pass any React node, use it in wrapper components, or even use it as a function for render props. React.Children utilities help manipulate children.
Render Props Pattern
Render props is a pattern where a component receives a function as a prop that returns React elements. The component calls this function instead of implementing its own render logic. What are Render Props: • Component receives function as prop • Function receives data/state as arguments • Component calls function to render • Allows sharing logic between components Benefits: • Share stateful logic • Flexible rendering • Separation of concerns • Reusable logic Common Use Cases: • Data fetching • Mouse tracking • Form state management • Theme providers
// Basic render prop
function MouseTracker({ render }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
return render(position);
}
function App() {
return (
<MouseTracker
render={({ x, y }) => (
<div>
Mouse position: {x}, {y}
</div>
)}
/>
);
}
// Render prop for data fetching
function DataFetcher({ url, render }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(data => {
setData(data);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, [url]);
return render({ data, loading, error });
}
function App() {
return (
<DataFetcher
url="/api/users"
render={({ data, loading, error }) => {
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <UserList users={data} />;
}}
/>
);
}
// Multiple render props
function Toggle({ on, toggle, children }) {
return children({ on, toggle });
}
function App() {
return (
<Toggle>
{({ on, toggle }) => (
<div>
<button onClick={toggle}>
{on ? 'ON' : 'OFF'}
</button>
{on && <div>Content is visible</div>}
</div>
)}
</Toggle>
);
}
// Render prop vs children as function
// These are equivalent:
function Component1({ render }) {
return render({ data: 'test' });
}
function Component2({ children }) {
return children({ data: 'test' });
}Render props allow sharing logic between components. The component receives a function that it calls with state/data. This pattern is powerful for reusable logic, though hooks often replace it in modern React.
Compound Components Pattern
Compound components are a pattern where multiple components work together to form a complete UI. They share implicit state and provide a flexible API. What are Compound Components: • Multiple components that work together • Share implicit state via Context • Flexible composition • Better API than monolithic component Benefits: • Flexible component structure • Better separation of concerns • Intuitive API • Composable and reusable Use Cases: • Select/Dropdown components • Tabs • Accordion • Form fields with labels and errors
// Compound Select component
const SelectContext = createContext();
function Select({ children, value, onChange }) {
const [isOpen, setIsOpen] = useState(false);
return (
<SelectContext.Provider value={{ value, onChange, isOpen, setIsOpen }}>
<div className="select">{children}</div>
</SelectContext.Provider>
);
}
function SelectTrigger({ children }) {
const { isOpen, setIsOpen } = useContext(SelectContext);
return (
<button onClick={() => setIsOpen(!isOpen)}>
{children}
</button>
);
}
function SelectOptions({ children }) {
const { isOpen } = useContext(SelectContext);
if (!isOpen) return null;
return <div className="options">{children}</div>;
}
function SelectOption({ value, children }) {
const { value: selectedValue, onChange, setIsOpen } = useContext(SelectContext);
return (
<div
className={`option ${value === selectedValue ? 'selected' : ''}`}
onClick={() => {
onChange(value);
setIsOpen(false);
}}
>
{children}
</div>
);
}
// Usage
function App() {
const [value, setValue] = useState('option1');
return (
<Select value={value} onChange={setValue}>
<SelectTrigger>
{value || 'Select an option'}
</SelectTrigger>
<SelectOptions>
<SelectOption value="option1">Option 1</SelectOption>
<SelectOption value="option2">Option 2</SelectOption>
<SelectOption value="option3">Option 3</SelectOption>
</SelectOptions>
</Select>
);
}
// Compound Tabs component
const TabsContext = createContext();
function Tabs({ children, defaultTab }) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
function TabsList({ children }) {
return <div className="tabs-list">{children}</div>;
}
function TabsTrigger({ value, children }) {
const { activeTab, setActiveTab } = useContext(TabsContext);
return (
<button
className={`tab ${activeTab === value ? 'active' : ''}`}
onClick={() => setActiveTab(value)}
>
{children}
</button>
);
}
function TabsContent({ value, children }) {
const { activeTab } = useContext(TabsContext);
if (activeTab !== value) return null;
return <div className="tab-content">{children}</div>;
}
// Usage
function App() {
return (
<Tabs defaultTab="tab1">
<TabsList>
<TabsTrigger value="tab1">Tab 1</TabsTrigger>
<TabsTrigger value="tab2">Tab 2</TabsTrigger>
</TabsList>
<TabsContent value="tab1">Content 1</TabsContent>
<TabsContent value="tab2">Content 2</TabsContent>
</Tabs>
);
}Compound components share state via Context and provide flexible composition. They create intuitive APIs where components work together naturally. This pattern is great for complex UI components.
Higher-Order Components (HOCs)
Higher-Order Components are functions that take a component and return a new component with additional functionality. They're a pattern for reusing component logic. What are HOCs: • Function that takes a component, returns a component • Adds functionality without modifying original • Shares logic between components • Common pattern before hooks Benefits: • Reusable logic • Separation of concerns • Can compose multiple HOCs • Works with any component Note: Hooks often replace HOCs in modern React, but HOCs are still useful in some cases.
// Basic HOC
function withLoading(Component) {
return function WithLoadingComponent({ isLoading, ...props }) {
if (isLoading) {
return <div>Loading...</div>;
}
return <Component {...props} />;
};
}
// Usage
const UserProfile = ({ user }) => <div>{user.name}</div>;
const UserProfileWithLoading = withLoading(UserProfile);
function App() {
return <UserProfileWithLoading isLoading={true} user={null} />;
}
// HOC with data fetching
function withData(url) {
return function(Component) {
return function WithDataComponent(props) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(data => {
setData(data);
setLoading(false);
});
}, []);
if (loading) return <div>Loading...</div>;
return <Component {...props} data={data} />;
};
};
}
// Usage
const UserList = ({ data }) => (
<ul>
{data.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
const UserListWithData = withData('/api/users')(UserList);
// HOC for authentication
function withAuth(Component) {
return function WithAuthComponent(props) {
const [isAuthenticated, setIsAuthenticated] = useState(false);
useEffect(() => {
// Check authentication
checkAuth().then(setIsAuthenticated);
}, []);
if (!isAuthenticated) {
return <div>Please log in</div>;
}
return <Component {...props} />;
};
}
// Composing HOCs
const EnhancedComponent = withAuth(withLoading(MyComponent));
// HOC vs Hook (modern approach)
// HOC:
const ComponentWithData = withData('/api/users')(MyComponent);
// Hook (preferred):
function MyComponent() {
const { data, loading } = useData('/api/users');
// ...
}HOCs add functionality to components. They're useful for cross-cutting concerns but hooks often provide a cleaner solution. Use HOCs when you need to enhance components in a reusable way.
Composition Best Practices
Following best practices makes your composed components more maintainable and easier to use. Best Practices: • Prefer composition over inheritance • Keep components small and focused • Use children for flexibility • Extract shared logic into hooks • Use compound components for complex UIs • Document component APIs • Provide sensible defaults • Make components composable Common Patterns: • Container/Presentational pattern • Provider pattern • Slot pattern (multiple children props) • Configuration objects
// ✅ GOOD: Small, focused components
function Button({ children, variant, size, onClick }) {
return (
<button
className={`btn btn-${variant} btn-${size}`}
onClick={onClick}
>
{children}
</button>
);
}
function IconButton({ icon, children, ...props }) {
return (
<Button {...props}>
<Icon name={icon} />
{children}
</Button>
);
}
// ✅ GOOD: Container/Presentational pattern
function UserListContainer() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchUsers().then(users => {
setUsers(users);
setLoading(false);
});
}, []);
return <UserList users={users} loading={loading} />;
}
function UserList({ users, loading }) {
if (loading) return <div>Loading...</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
// ✅ GOOD: Slot pattern
function Card({ header, footer, children }) {
return (
<div className="card">
{header && <div className="card-header">{header}</div>}
<div className="card-body">{children}</div>
{footer && <div className="card-footer">{footer}</div>}
</div>
);
}
function App() {
return (
<Card
header={<h2>Title</h2>}
footer={<button>Action</button>}
>
Content here
</Card>
);
}
// ✅ GOOD: Configuration object for flexibility
function FormField({ label, error, children, config }) {
return (
<div className="form-field">
<label>{label}</label>
{children}
{error && <span className="error">{error}</span>}
{config?.helpText && <span className="help">{config.helpText}</span>}
</div>
);
}Follow best practices: keep components small, use composition patterns appropriately, extract logic into hooks, and make components flexible and reusable. Document your component APIs clearly.
Conclusion
Component composition is fundamental to building maintainable React applications. Use children for flexibility, render props for sharing logic, compound components for complex UIs, and HOCs when appropriate. Prefer composition over inheritance, keep components small and focused, and extract shared logic into hooks. Remember: good composition creates flexible, reusable components that are easy to understand and maintain.