- Published on
React Composition Pattern
- Authors
- Name
- Zairyl Zafra
- @zrylzfra
A Story of Building Better Components
Sarah was building a dashboard for her company. She started with a simple card component, but soon found herself drowning in prop hell.
"I need a card with a title and body," she thought. Easy enough:
<Card title="Welcome" body="Hello there!" />
But then the requirements changed. "The title needs an icon. Oh, and the body should have a button sometimes. And can the footer be different colors?"
Her component became a monster:
<Card
title="Welcome"
titleIcon="star"
body="Hello there!"
showButton={true}
buttonText="Click me"
footerColor="blue"
footerText="Footer here"
// ... 20 more props
/>
The Turning Point
A senior developer showed Sarah a different way: composition. Instead of passing everything as props, why not pass components as children?
<Card>
<CardHeader>
<Icon name="star" />
<h2>Welcome</h2>
</CardHeader>
<CardBody>
<p>Hello there!</p>
<Button>Click me</Button>
</CardBody>
<CardFooter color="blue">Footer here</CardFooter>
</Card>
"This is just like HTML," Sarah realized. "You don't pass the contents of a <div> as props—you put things inside it!"
Non-Compositional vs. Compositional: A Side-by-Side Comparison
To truly understand the difference, let's examine the same component built both ways.
Example 1: The Modal Component
Non-Compositional Approach (Props-Heavy):
function Modal({
isOpen,
title,
titleIcon,
titleColor,
message,
messageAlign,
showConfirmButton,
confirmText,
confirmColor,
confirmIcon,
onConfirm,
showCancelButton,
cancelText,
cancelColor,
onCancel,
showFooter,
footerText,
footerAlign,
allowClose,
onClose,
size,
className,
// ... 20 more props
}) {
if (!isOpen) return null;
return (
<div className={`modal modal--${size} ${className}`}>
<div className="modal-content">
{allowClose && (
<button className="modal-close" onClick={onClose}>×</button>
)}
{title && (
<div className="modal-header" style={{ color: titleColor }}>
{titleIcon && <Icon name={titleIcon} />}
<h2>{title}</h2>
</div>
)}
{message && (
<div className="modal-body" style={{ textAlign: messageAlign }}>
<p>{message}</p>
</div>
)}
{(showConfirmButton || showCancelButton) && (
<div className="modal-actions">
{showCancelButton && (
<button
className={`btn btn--${cancelColor}`}
onClick={onCancel}
>
{cancelText || 'Cancel'}
</button>
)}
{showConfirmButton && (
<button
className={`btn btn--${confirmColor}`}
onClick={onConfirm}
>
{confirmIcon && <Icon name={confirmIcon} />}
{confirmText || 'Confirm'}
</button>
)}
</div>
)}
{showFooter && footerText && (
<div className="modal-footer" style={{ textAlign: footerAlign }}>
{footerText}
</div>
)}
</div>
</div>
);
}
// Usage - verbose and inflexible
<Modal
isOpen={true}
title="Delete Account"
titleIcon="warning"
titleColor="red"
message="This action cannot be undone. Are you absolutely sure?"
messageAlign="center"
showConfirmButton={true}
confirmText="Yes, delete my account"
confirmColor="danger"
onConfirm={handleDelete}
showCancelButton={true}
cancelText="No, keep it"
onCancel={handleCancel}
allowClose={true}
onClose={handleCancel}
size="medium"
/>
// What if you need a form inside? Add more props!
<Modal
isOpen={true}
title="Edit Profile"
showForm={true}
formFields={[...]}
formValidation={...}
onFormSubmit={...}
// Props explosion continues...
/>
Compositional Approach:
// Simple, focused components
function Modal({ isOpen, onClose, size = 'medium', children }) {
if (!isOpen) return null;
return (
<div className={`modal modal--${size}`}>
<div className="modal-content">
<button className="modal-close" onClick={onClose}>×</button>
{children}
</div>
</div>
);
}
function ModalHeader({ children, color }) {
return (
<div className="modal-header" style={{ color }}>
{children}
</div>
);
}
function ModalBody({ children, align }) {
return (
<div className="modal-body" style={{ textAlign: align }}>
{children}
</div>
);
}
function ModalActions({ children }) {
return <div className="modal-actions">{children}</div>;
}
function ModalFooter({ children, align }) {
return (
<div className="modal-footer" style={{ textAlign: align }}>
{children}
</div>
);
}
// Usage - clean and flexible
<Modal isOpen={true} onClose={handleCancel}>
<ModalHeader color="red">
<Icon name="warning" />
<h2>Delete Account</h2>
</ModalHeader>
<ModalBody align="center">
<p>This action cannot be undone.</p>
<p>Are you absolutely sure?</p>
</ModalBody>
<ModalActions>
<Button variant="secondary" onClick={handleCancel}>
No, keep it
</Button>
<Button variant="danger" onClick={handleDelete}>
<Icon name="trash" />
Yes, delete my account
</Button>
</ModalActions>
</Modal>
// Need a form? Just put it inside! No new props needed
<Modal isOpen={true} onClose={handleClose}>
<ModalHeader>
<h2>Edit Profile</h2>
</ModalHeader>
<ModalBody>
<form onSubmit={handleSubmit}>
<Input label="Name" value={name} onChange={setName} />
<Input label="Email" value={email} onChange={setEmail} />
<TextArea label="Bio" value={bio} onChange={setBio} />
<ModalActions>
<Button type="button" onClick={handleClose}>Cancel</Button>
<Button type="submit">Save Changes</Button>
</ModalActions>
</form>
</ModalBody>
</Modal>
// Different layout? No problem!
<Modal isOpen={true} onClose={handleClose} size="large">
<ModalBody>
<div className="two-column-layout">
<div className="column">
<h3>Preview</h3>
<ImagePreview src={image} />
</div>
<div className="column">
<h3>Details</h3>
<ImageMetadata data={metadata} />
</div>
</div>
</ModalBody>
<ModalFooter>
<Button onClick={handleDownload}>Download</Button>
</ModalFooter>
</Modal>
Example 2: The Alert/Notification Component
Non-Compositional Approach:
function Alert({
type, // 'success' | 'error' | 'warning' | 'info'
title,
message,
showIcon,
icon,
iconPosition,
showCloseButton,
onClose,
showAction,
actionText,
actionVariant,
onAction,
showSecondaryAction,
secondaryActionText,
onSecondaryAction,
dismissible,
autoDismiss,
dismissAfter,
animateIn,
animateOut,
position,
fullWidth,
bordered,
elevated,
// ...more props
}) {
const [isVisible, setIsVisible] = useState(true);
useEffect(() => {
if (autoDismiss) {
setTimeout(() => setIsVisible(false), dismissAfter);
}
}, [autoDismiss, dismissAfter]);
if (!isVisible) return null;
const iconMap = {
success: 'check-circle',
error: 'x-circle',
warning: 'alert-triangle',
info: 'info-circle'
};
const displayIcon = icon || iconMap[type];
return (
<div
className={`
alert alert--${type}
${position ? `alert--${position}` : ''}
${fullWidth ? 'alert--full-width' : ''}
${bordered ? 'alert--bordered' : ''}
${elevated ? 'alert--elevated' : ''}
${animateIn ? 'alert--animate-in' : ''}
`}
>
{showIcon && iconPosition === 'left' && (
<Icon name={displayIcon} />
)}
<div className="alert-content">
{title && <h4>{title}</h4>}
{message && <p>{message}</p>}
{(showAction || showSecondaryAction) && (
<div className="alert-actions">
{showSecondaryAction && (
<button onClick={onSecondaryAction}>
{secondaryActionText}
</button>
)}
{showAction && (
<button
className={`btn--${actionVariant}`}
onClick={onAction}
>
{actionText}
</button>
)}
</div>
)}
</div>
{showIcon && iconPosition === 'right' && (
<Icon name={displayIcon} />
)}
{(dismissible || showCloseButton) && (
<button className="alert-close" onClick={onClose}>×</button>
)}
</div>
);
}
// Usage - trying to handle every scenario through props
<Alert
type="error"
title="Payment Failed"
message="Your credit card was declined."
showIcon={true}
showAction={true}
actionText="Update Payment Method"
onAction={handleUpdatePayment}
showCloseButton={true}
onClose={handleClose}
elevated={true}
/>
// What if you need custom content? Add more props!
<Alert
type="info"
customContent={<ComplexComponent />} // Escape hatch
// Now you have two systems: props AND custom content
/>
Compositional Approach:
// Focused, single-responsibility components
function Alert({ variant = 'info', children, onClose, dismissible }) {
return (
<div className={`alert alert--${variant}`}>
{children}
{dismissible && (
<button className="alert-close" onClick={onClose}>×</button>
)}
</div>
);
}
function AlertIcon({ name, variant }) {
const iconMap = {
success: 'check-circle',
error: 'x-circle',
warning: 'alert-triangle',
info: 'info-circle'
};
return <Icon name={name || iconMap[variant]} />;
}
function AlertTitle({ children }) {
return <h4 className="alert-title">{children}</h4>;
}
function AlertDescription({ children }) {
return <div className="alert-description">{children}</div>;
}
function AlertActions({ children }) {
return <div className="alert-actions">{children}</div>;
}
// Usage - clean and infinitely flexible
<Alert variant="error" dismissible onClose={handleClose}>
<AlertIcon variant="error" />
<div>
<AlertTitle>Payment Failed</AlertTitle>
<AlertDescription>
Your credit card was declined.
</AlertDescription>
<AlertActions>
<Button onClick={handleUpdatePayment}>
Update Payment Method
</Button>
</AlertActions>
</div>
</Alert>
// Complex custom content? No problem, no escape hatches needed!
<Alert variant="info">
<AlertIcon variant="info" />
<div>
<AlertTitle>System Maintenance</AlertTitle>
<AlertDescription>
<p>We'll be performing maintenance on:</p>
<ul>
<li>Database servers: 2-3 AM EST</li>
<li>API endpoints: 3-4 AM EST</li>
</ul>
<p>Expected downtime: <strong>2 hours</strong></p>
</AlertDescription>
<AlertActions>
<a href="/status">View Status Page</a>
<Button onClick={handleSubscribe}>
Subscribe to Updates
</Button>
</AlertActions>
</div>
</Alert>
// Different layout entirely? Just arrange differently!
<Alert variant="success">
<div className="horizontal-alert">
<AlertIcon variant="success" />
<AlertDescription>
Successfully saved! <a href="/view">View changes</a>
</AlertDescription>
</div>
</Alert>
The Key Differences: A Comparison Table
| Aspect | Non-Compositional | Compositional |
|---|---|---|
| Flexibility | Limited to pre-defined props | Infinite combinations |
| API Surface | 20-50+ props per component | 2-5 props per component |
| Learning Curve | Must learn all props upfront | Learn by composing familiar pieces |
| Maintainability | Changes require modifying component internals | Add new components, existing ones unchanged |
| Testing | Must test every prop combination | Test small components independently |
| Customization | Need "escape hatch" props for edge cases | Everything is already customizable |
| Bundle Size | Large components with conditional logic | Small, tree-shakeable components |
| Predictability | Complex internal logic, hidden behavior | What you see is what you get |
| Reusability | Components are specific, tightly coupled | Components are generic building blocks |
Real Impact on Development
Scenario: Product Manager requests a change
"Can we add a video preview to the modal?"
Non-Compositional Response:
// Add new props to Modal component
function Modal({
// ... 30 existing props
showVideo,
videoUrl,
videoAutoplay,
videoControls,
videoThumbnail,
// Now testing 35+ prop combinations...
}) {
// Add conditional rendering logic
// Hope it doesn't break existing usage
}
Compositional Response:
// No changes needed to any existing components!
<Modal isOpen={true}>
<ModalHeader>
<h2>Product Demo</h2>
</ModalHeader>
<ModalBody>
<VideoPlayer src={videoUrl} autoplay controls thumbnail={thumbnail} />
</ModalBody>
</Modal>
The compositional approach just works. No changes, no new props, no risk of breaking existing code.
Why Composition Wins
1. Flexibility Without Complexity
With composition, Sarah's Card component became simple:
function Card({ children }) {
return <div className="card">{children}</div>
}
function CardHeader({ children }) {
return <div className="card-header">{children}</div>
}
No conditional rendering. No prop explosion. Just simple containers.
2. Better Readability
Compare these two approaches:
Props approach:
<Modal
isOpen={true}
title="Delete Item"
message="Are you sure?"
confirmText="Yes, delete"
cancelText="Cancel"
onConfirm={handleDelete}
onCancel={handleCancel}
/>
Composition approach:
<Modal isOpen={true}>
<ModalTitle>Delete Item</ModalTitle>
<ModalBody>Are you sure?</ModalBody>
<ModalActions>
<Button onClick={handleCancel}>Cancel</Button>
<Button variant="danger" onClick={handleDelete}>
Yes, delete
</Button>
</ModalActions>
</Modal>
The second version reads like a story. You can see the structure immediately.
3. Reusability
Sarah's individual components became reusable building blocks:
// Use CardHeader anywhere
<Dialog>
<CardHeader>
<Icon name="info" />
<h2>Information</h2>
</CardHeader>
</Dialog>
Real-World Scenario: Building a Product Card
Let's see composition in action. Sarah needs to build product cards for an e-commerce site.
The Requirements
- Show product image, name, price, and rating
- Some products are on sale (show badge)
- Some products are out of stock (disable button)
- Premium products get special styling
The Composition Solution
// Simple building blocks
function ProductCard({ children, isPremium }) {
return <div className={`product-card ${isPremium ? 'premium' : ''}`}>{children}</div>
}
function ProductImage({ src, alt, badge }) {
return (
<div className="product-image">
<img src={src} alt={alt} />
{badge && <span className="badge">{badge}</span>}
</div>
)
}
function ProductInfo({ children }) {
return <div className="product-info">{children}</div>
}
function ProductPrice({ original, sale }) {
return (
<div className="product-price">
{sale ? (
<>
<span className="original">${original}</span>
<span className="sale">${sale}</span>
</>
) : (
<span>${original}</span>
)}
</div>
)
}
function ProductActions({ children }) {
return <div className="product-actions">{children}</div>
}
Using the Components
Now Sarah can build any variation she needs:
// Regular product
<ProductCard>
<ProductImage src="shoe.jpg" alt="Running Shoes" />
<ProductInfo>
<h3>Running Shoes</h3>
<Rating value={4.5} />
<ProductPrice original={89.99} />
</ProductInfo>
<ProductActions>
<Button>Add to Cart</Button>
</ProductActions>
</ProductCard>
// Product on sale
<ProductCard>
<ProductImage
src="jacket.jpg"
alt="Winter Jacket"
badge="SALE"
/>
<ProductInfo>
<h3>Winter Jacket</h3>
<Rating value={5} />
<ProductPrice original={199.99} sale={149.99} />
</ProductInfo>
<ProductActions>
<Button>Add to Cart</Button>
</ProductActions>
</ProductCard>
// Premium product, out of stock
<ProductCard isPremium>
<ProductImage src="watch.jpg" alt="Luxury Watch" />
<ProductInfo>
<h3>Luxury Watch</h3>
<Rating value={4.8} />
<ProductPrice original={1299.99} />
<p className="stock-status">Out of Stock</p>
</ProductInfo>
<ProductActions>
<Button disabled>Notify Me</Button>
</ProductActions>
</ProductCard>
The Key Insights
Through her journey, Sarah discovered why composition is powerful:
1. Think in Components, Not Props
- Instead of "What props does this need?" ask "What parts does this have?"
2. Embrace the Children Prop
childrenis just a prop, but it's special—it lets you nest components naturally
3. Keep Components Focused
- Each component does one thing well
- Combine them to create complex UIs
4. Let Users Control Layout
- Don't dictate how components are arranged
- Provide building blocks and let users compose
The Transformation
Sarah's components became:
- Easier to understand - each piece has a clear purpose
- Easier to test - small components with minimal logic
- Easier to change - modify one piece without breaking others
- Easier to reuse - mix and match components anywhere
She had learned that the best React components aren't the ones with the most features—they're the ones that compose well with others.
Complex Real-World Scenarios
After mastering the basics, Sarah encountered more challenging situations where composition truly proved its worth.
Scenario 1: Multi-Step Form Wizard
Sarah needed to build a complex onboarding flow with validation, progress tracking, and conditional steps.
The Challenge: Different users see different steps based on their role. Some steps have sub-steps. Each step needs its own validation.
The Composition Solution:
// Core wizard components
function Wizard({ children, onComplete }) {
const [currentStep, setCurrentStep] = useState(0)
const [formData, setFormData] = useState({})
const steps = React.Children.toArray(children)
const CurrentStepComponent = steps[currentStep]
const next = (data) => {
setFormData((prev) => ({ ...prev, ...data }))
if (currentStep < steps.length - 1) {
setCurrentStep((prev) => prev + 1)
} else {
onComplete({ ...formData, ...data })
}
}
const back = () => setCurrentStep((prev) => Math.max(0, prev - 1))
return (
<div className="wizard">
<WizardProgress current={currentStep} total={steps.length} />
{React.cloneElement(CurrentStepComponent, {
onNext: next,
onBack: back,
data: formData,
isFirst: currentStep === 0,
isLast: currentStep === steps.length - 1,
})}
</div>
)
}
function WizardStep({ title, children, onNext, onBack, isFirst, isLast, data }) {
const [stepData, setStepData] = useState({})
const [errors, setErrors] = useState({})
const handleSubmit = (e) => {
e.preventDefault()
// Validate and proceed
const validationErrors = children.props.validate?.(stepData) || {}
if (Object.keys(validationErrors).length === 0) {
onNext(stepData)
} else {
setErrors(validationErrors)
}
}
return (
<div className="wizard-step">
<h2>{title}</h2>
<form onSubmit={handleSubmit}>
{React.cloneElement(children, {
data: stepData,
onChange: setStepData,
errors,
})}
<div className="wizard-actions">
{!isFirst && (
<Button type="button" onClick={onBack}>
Back
</Button>
)}
<Button type="submit">{isLast ? 'Complete' : 'Next'}</Button>
</div>
</form>
</div>
)
}
function WizardProgress({ current, total }) {
return (
<div className="wizard-progress">
{Array.from({ length: total }, (_, i) => (
<div key={i} className={`step ${i <= current ? 'active' : ''}`} />
))}
</div>
)
}
Usage - Different flows for different users:
// For a customer
<Wizard onComplete={handleCustomerSignup}>
<WizardStep title="Account Details">
<AccountForm validate={validateAccount} />
</WizardStep>
<WizardStep title="Personal Info">
<PersonalInfoForm validate={validatePersonal} />
</WizardStep>
<WizardStep title="Preferences">
<PreferencesForm />
</WizardStep>
</Wizard>
// For a business user - completely different steps
<Wizard onComplete={handleBusinessSignup}>
<WizardStep title="Company Information">
<CompanyForm validate={validateCompany} />
</WizardStep>
<WizardStep title="Team Members">
<TeamMembersForm validate={validateTeam} />
</WizardStep>
<WizardStep title="Billing">
<BillingForm validate={validateBilling} />
</WizardStep>
<WizardStep title="Integration">
<IntegrationForm />
</WizardStep>
</Wizard>
Scenario 2: Data Table with Complex Features
Sarah's team needed a table that supported sorting, filtering, pagination, row selection, expandable rows, and custom cell renderers.
The Challenge: Every feature combination creates exponential complexity. The table needs to work with any data type.
The Composition Solution:
// Flexible table system
function DataTable({ data, children }) {
const [sortConfig, setSortConfig] = useState(null)
const [selectedRows, setSelectedRows] = useState(new Set())
const [expandedRows, setExpandedRows] = useState(new Set())
const contextValue = {
data,
sortConfig,
setSortConfig,
selectedRows,
setSelectedRows,
expandedRows,
setExpandedRows,
}
return (
<TableContext.Provider value={contextValue}>
<div className="data-table">{children}</div>
</TableContext.Provider>
)
}
function TableToolbar({ children }) {
return <div className="table-toolbar">{children}</div>
}
function TableFilters({ children }) {
return <div className="table-filters">{children}</div>
}
function TableHeader({ children }) {
return (
<thead>
<tr>{children}</tr>
</thead>
)
}
function TableColumn({ field, sortable, children }) {
const { sortConfig, setSortConfig } = useContext(TableContext)
const handleSort = () => {
if (!sortable) return
setSortConfig({
field,
direction: sortConfig?.field === field && sortConfig.direction === 'asc' ? 'desc' : 'asc',
})
}
return (
<th onClick={handleSort} className={sortable ? 'sortable' : ''}>
{children}
{sortable && sortConfig?.field === field && (
<span>{sortConfig.direction === 'asc' ? '↑' : '↓'}</span>
)}
</th>
)
}
function TableBody({ children, renderRow }) {
const { data, sortConfig, selectedRows, expandedRows } = useContext(TableContext)
let processedData = [...data]
if (sortConfig) {
processedData.sort((a, b) => {
const aVal = a[sortConfig.field]
const bVal = b[sortConfig.field]
const modifier = sortConfig.direction === 'asc' ? 1 : -1
return aVal > bVal ? modifier : -modifier
})
}
return (
<tbody>
{processedData.map((row, index) => (
<React.Fragment key={row.id || index}>
{renderRow(row, {
isSelected: selectedRows.has(row.id),
isExpanded: expandedRows.has(row.id),
})}
</React.Fragment>
))}
</tbody>
)
}
function TableRow({ id, children, expandable, expandedContent }) {
const { selectedRows, setSelectedRows, expandedRows, setExpandedRows } = useContext(TableContext)
const isSelected = selectedRows.has(id)
const isExpanded = expandedRows.has(id)
const toggleExpand = () => {
setExpandedRows((prev) => {
const next = new Set(prev)
if (next.has(id)) next.delete(id)
else next.add(id)
return next
})
}
return (
<>
<tr className={isSelected ? 'selected' : ''}>
{expandable && (
<td>
<button onClick={toggleExpand}>{isExpanded ? '−' : '+'}</button>
</td>
)}
{children}
</tr>
{expandable && isExpanded && (
<tr className="expanded-row">
<td colSpan="100%">{expandedContent}</td>
</tr>
)}
</>
)
}
Usage - Building a complex user management table:
function UserManagementTable() {
const [users, setUsers] = useState([])
const [filters, setFilters] = useState({ role: 'all', status: 'all' })
const filteredUsers = users.filter((user) => {
if (filters.role !== 'all' && user.role !== filters.role) return false
if (filters.status !== 'all' && user.status !== filters.status) return false
return true
})
return (
<DataTable data={filteredUsers}>
<TableToolbar>
<h2>User Management</h2>
<Button onClick={handleExport}>Export CSV</Button>
</TableToolbar>
<TableFilters>
<Select value={filters.role} onChange={(role) => setFilters((f) => ({ ...f, role }))}>
<option value="all">All Roles</option>
<option value="admin">Admin</option>
<option value="user">User</option>
</Select>
<Select value={filters.status} onChange={(status) => setFilters((f) => ({ ...f, status }))}>
<option value="all">All Status</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</Select>
</TableFilters>
<table>
<TableHeader>
<TableColumn field="name" sortable>
Name
</TableColumn>
<TableColumn field="email" sortable>
Email
</TableColumn>
<TableColumn field="role" sortable>
Role
</TableColumn>
<TableColumn field="lastLogin" sortable>
Last Login
</TableColumn>
<TableColumn>Actions</TableColumn>
</TableHeader>
<TableBody
renderRow={(user) => (
<TableRow id={user.id} expandable expandedContent={<UserDetailsPanel user={user} />}>
<td>{user.name}</td>
<td>{user.email}</td>
<td>
<Badge>{user.role}</Badge>
</td>
<td>{formatDate(user.lastLogin)}</td>
<td>
<IconButton onClick={() => handleEdit(user)}>✏️</IconButton>
<IconButton onClick={() => handleDelete(user)}>🗑️</IconButton>
</td>
</TableRow>
)}
/>
</table>
</DataTable>
)
}
Scenario 3: Permission-Based UI System
Sarah's enterprise app needed complex permission controls where different users see different UI elements.
The Challenge: Permissions are hierarchical and contextual. Some features require multiple permissions. Need to avoid repeating permission logic everywhere.
The Composition Solution:
// Permission wrapper components
function PermissionGate({ requires, fallback = null, children }) {
const { hasPermission } = usePermissions()
// Handle single permission or array
const permissions = Array.isArray(requires) ? requires : [requires]
const hasAccess = permissions.every((p) => hasPermission(p))
if (!hasAccess) return fallback
return <>{children}</>
}
function AnyPermission({ requires, fallback = null, children }) {
const { hasPermission } = usePermissions()
const permissions = Array.isArray(requires) ? requires : [requires]
const hasAccess = permissions.some((p) => hasPermission(p))
if (!hasAccess) return fallback
return <>{children}</>
}
function RoleGate({ roles, fallback = null, children }) {
const { hasRole } = usePermissions()
const roleList = Array.isArray(roles) ? roles : [roles]
const hasAccess = roleList.some((r) => hasRole(r))
if (!hasAccess) return fallback
return <>{children}</>
}
// Composable feature flags
function FeatureFlag({ flag, fallback = null, children }) {
const { isEnabled } = useFeatureFlags()
if (!isEnabled(flag)) return fallback
return <>{children}</>
}
// Smart component that combines permissions and features
function ProtectedFeature({ requires, feature, role, fallback = null, children }) {
return (
<PermissionGate requires={requires} fallback={fallback}>
<FeatureFlag flag={feature} fallback={fallback}>
<RoleGate roles={role} fallback={fallback}>
{children}
</RoleGate>
</FeatureFlag>
</PermissionGate>
)
}
Usage - Building a dashboard with complex access control:
function Dashboard() {
return (
<div className="dashboard">
<Header>
<Logo />
{/* Only admins and managers see analytics */}
<AnyPermission requires={['view_analytics', 'manage_team']}>
<NavLink to="/analytics">Analytics</NavLink>
</AnyPermission>
{/* Feature flag + permission check */}
<ProtectedFeature
requires="beta_access"
feature="new_reports"
fallback={<NavLink to="/reports">Reports (Classic)</NavLink>}
>
<NavLink to="/reports-v2">Reports (Beta)</NavLink>
</ProtectedFeature>
{/* Admin-only section */}
<RoleGate roles="admin">
<NavLink to="/admin">Admin Panel</NavLink>
</RoleGate>
</Header>
<MainContent>
{/* Different widgets based on permissions */}
<PermissionGate requires="view_sales">
<SalesWidget />
</PermissionGate>
<PermissionGate requires="view_team_performance" fallback={<RestrictedMessage />}>
<TeamPerformanceWidget />
</PermissionGate>
{/* Requires ALL permissions */}
<PermissionGate requires={['view_finances', 'view_forecasts']}>
<FinancialForecastWidget />
</PermissionGate>
{/* Requires ANY permission */}
<AnyPermission requires={['create_content', 'edit_content']}>
<ContentManagementWidget />
</AnyPermission>
{/* Complex nested permissions */}
<PermissionGate requires="view_reports">
<ReportsSection>
<PermissionGate requires="export_reports">
<ExportButton />
</PermissionGate>
<PermissionGate requires="share_reports">
<ShareButton />
</PermissionGate>
<FeatureFlag flag="advanced_filters">
<AdvancedFilterPanel />
</FeatureFlag>
</ReportsSection>
</PermissionGate>
</MainContent>
</div>
)
}
The Lessons Learned
Through these complex scenarios, Sarah discovered deeper truths about composition:
1. Composition Scales Beautifully
- Simple components combine to handle complex requirements
- No need to predict every use case upfront
2. Context + Composition = Power
- Use React Context to share state between composed components
- Keeps the API clean while enabling deep integration
3. Flexibility Through Constraints
- Each component does one thing well
- Combining them creates infinite possibilities
4. Real-World Complexity Requires Real-World Patterns
- Don't build generic "configure everything" components
- Build focused components that compose into solutions
Sarah's codebase transformed from a tangled mess of props and conditionals into an elegant system of composable pieces. Her team could now build new features in hours instead of days, and maintenance became a joy instead of a nightmare.
"Make it work, then make it right, then make it fast. But most importantly, make it composable." - The path to maintainable React code.