Component Composition
Learn how to use compound components and composition patterns to build flexible, reusable UI components.
What is Component Composition?
Component composition is a pattern where complex components are built by combining simpler, focused components that work together. Matrix UI extensively uses compound components to provide flexibility while maintaining consistency.
Compound Components
Compound components are a group of components that work together to form a complete UI pattern. They share implicit state and communicate with each other internally.
Card Component Family
The Card component demonstrates the compound pattern perfectly:
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
CardFooter
} from '@matrix-ui/components'
function UserProfile() {
return (
<Card>
<CardHeader>
<CardTitle>John Doe</CardTitle>
<CardDescription>Senior Developer</CardDescription>
</CardHeader>
<CardContent>
<p>Building amazing web applications with React and TypeScript.</p>
</CardContent>
<CardFooter>
<Button>Contact</Button>
<Button variant="outline">View Profile</Button>
</CardFooter>
</Card>
)
}
Dialog Component Family
import {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
DialogClose
} from '@matrix-ui/components'
function ConfirmDialog() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="destructive">Delete Account</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you sure?</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently delete
your account and remove your data from our servers.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button variant="destructive">Delete Account</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
Alert Component Family
import { Alert, AlertTitle, AlertDescription } from '@matrix-ui/components'
import { AlertTriangle } from 'lucide-react'
function StatusAlert() {
return (
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>
Your session has expired. Please log in again.
</AlertDescription>
</Alert>
)
}
Composition Benefits
Flexibility
Components can be rearranged and customized
- • Mix and match sub-components as needed
- • Skip optional parts when not required
- • Customize individual component props
- • Override default behavior selectively
Maintainability
Easier to understand and modify
- • Single responsibility principle
- • Clear component boundaries
- • Isolated concerns
- • Easy to test individual parts
Reusability
Components work in different contexts
- • Sub-components can be used independently
- • Consistent API across similar patterns
- • Standardized composition patterns
- • Reduced code duplication
Composition Patterns
Slot Pattern
Use slots to define flexible content areas:
// Good: Using slots for flexible layout
<Card>
<CardHeader>
{/* Header slot - optional */}
<CardTitle>User Settings</CardTitle>
</CardHeader>
<CardContent>
{/* Content slot - main content */}
<SettingsForm />
</CardContent>
<CardFooter>
{/* Footer slot - actions */}
<Button>Save</Button>
<Button variant="outline">Cancel</Button>
</CardFooter>
</Card>
// Bad: Monolithic component
<SettingsCard
title="User Settings"
content={<SettingsForm />}
primaryAction="Save"
secondaryAction="Cancel"
/>
Provider Pattern
Some compound components use React context for internal communication:
// Dialog uses context internally
<Dialog> {/* Provider */}
<DialogTrigger> {/* Consumer */}
<Button>Open Dialog</Button>
</DialogTrigger>
<DialogContent> {/* Consumer */}
<DialogTitle>Dialog Title</DialogTitle>
</DialogContent>
</Dialog>
// Context shares state between components:
// - Open/close state
// - Focus management
// - Accessibility attributes
Render Props Pattern
Use render props for dynamic content:
// Custom composition using render props
function DataCard({ data, renderHeader, renderContent }) {
return (
<Card>
{renderHeader && (
<CardHeader>
{renderHeader(data)}
</CardHeader>
)}
<CardContent>
{renderContent(data)}
</CardContent>
</Card>
)
}
// Usage
<DataCard
data={user}
renderHeader={(user) => (
<>
<CardTitle>{user.name}</CardTitle>
<CardDescription>{user.role}</CardDescription>
</>
)}
renderContent={(user) => (
<UserProfile user={user} />
)}
/>
Advanced Composition
Conditional Rendering
function ProductCard({ product, showActions = true, showDescription = true }) {
return (
<Card>
<CardHeader>
<CardTitle>{product.name}</CardTitle>
{showDescription && product.description && (
<CardDescription>{product.description}</CardDescription>
)}
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">${product.price}</div>
</CardContent>
{showActions && (
<CardFooter>
<Button>Add to Cart</Button>
<Button variant="outline">View Details</Button>
</CardFooter>
)}
</Card>
)
}
Dynamic Composition
function DynamicAlert({ type, title, description, actions }) {
return (
<Alert variant={type}>
{title && <AlertTitle>{title}</AlertTitle>}
{description && <AlertDescription>{description}</AlertDescription>}
{actions && (
<div className="flex gap-2 mt-2">
{actions.map((action, index) => (
<Button key={index} {...action.props}>
{action.label}
</Button>
))}
</div>
)}
</Alert>
)
}
// Usage
<DynamicAlert
type="destructive"
title="Authentication Error"
description="Please check your credentials and try again."
actions={[
{ label: "Retry", props: { onClick: handleRetry } },
{ label: "Cancel", props: { variant: "outline", onClick: handleCancel } }
]}
/>
Best Practices
1. Keep Components Focused
- Each component should have a single responsibility
- Avoid creating overly complex monolithic components
- Prefer composition over complex prop APIs
2. Maintain Consistent APIs
- Use consistent naming patterns across component families
- Follow established prop conventions
- Provide sensible defaults for optional components
3. Consider Accessibility
- Ensure proper ARIA relationships between components
- Handle keyboard navigation across compound components
- Maintain focus management in interactive patterns
4. Performance Considerations
- Use React.memo() for frequently rendered sub-components
- Avoid unnecessary re-renders by optimizing context usage
- Consider lazy loading for optional sub-components