Composition over configuration: best practices explained
When you find yourself staring at a React component with twenty boolean props, each one controlling a slightly different visual state or behavior, you've hit a common wall. The principle of composition over configuration: best practices explained offers a clearer path forward—one that keeps your codebase maintainable, your components reusable, and your team sane. In this article, we'll walk through practical patterns that replace sprawling prop lists with composable, predictable component hierarchies.
The problem with configuration-heavy components
Configuration-driven design feels efficient at first. You create a flexible Button or Card, add props to control every possible variation, and ship it. Months later, that component has grown to accept:
isLoading,isDisabled,isActiveshowBorder,showShadow,showIconvariant,size,color,alignmentonHover,onClick,onFocus,onBlur
The component becomes a catch-all, harder to reason about, and often harder to test. Every new feature request triggers a new prop. Meanwhile, you're duplicating logic across components because they're too specialized to share.
Composition inverts this model. Instead of configuring a single omniscient component, you build small, focused pieces and nest them intentionally. This approach scales better and reflects how teams actually think about UI.
Pass children instead of adding props
The simplest move toward composition is replacing props with the children prop. When you have a layout component or container, don't create a prop for every slot—let consumers pass content directly.
Before (configuration-heavy):
<Alert
type="warning"
title="Attention"
message="This action cannot be undone"
icon={<WarningIcon />}
onDismiss={handleClose}
/>The component definition becomes a kitchen sink:
interface AlertProps {
type: 'warning' | 'error' | 'success';
title: string;
message: string;
icon?: React.ReactNode;
onDismiss?: () => void;
}
export const Alert: React.FC<AlertProps> = ({
type,
title,
message,
icon,
onDismiss,
}) => {
return (
<div className={`alert alert-${type}`}>
{icon && <div className="alert-icon">{icon}</div>}
<div className="alert-content">
<h3>{title}</h3>
<p>{message}</p>
</div>
{onDismiss && (
<button onClick={onDismiss}>Dismiss</button>
)}
</div>
);
};After (composition-friendly):
<Alert type="warning" onDismiss={handleClose}>
<WarningIcon />
<div>
<h3>Attention</h3>
<p>This action cannot be undone</p>
</div>
</Alert>The component now owns only layout and styling, not content structure:
interface AlertProps {
type: 'warning' | 'error' | 'success';
onDismiss?: () => void;
children: React.ReactNode;
}
export const Alert: React.FC<AlertProps> = ({
type,
onDismiss,
children,
}) => {
return (
<div className={`alert alert-${type}`}>
<div className="alert-content">{children}</div>
{onDismiss && (
<button onClick={onDismiss}>Dismiss</button>
)}
</div>
);
};This shift immediately reduces cognitive load. The component's contract is clearer: it handles appearance and behavior, consumers handle content. You can now use the same Alert with text, rich elements, forms, or anything else without adding new props.
Slots: header, footer, and actions as props
Some components have multiple content regions that aren't just "children." A modal might have a header, body, and footer. A card might have a header, body, and action area. Rather than creating a bloated prop for every permutation, use slot props—dedicated props that accept React elements.
Consider a common dashboard card:
<Card
header={<div>Orders</div>}
footer={<span>Last updated: 2 mins ago</span>}
actions={<button>Export</button>}
>
{/* order list */}
</Card>The implementation uses named slots:
interface CardProps {
header?: React.ReactNode;
footer?: React.ReactNode;
actions?: React.ReactNode;
children: React.ReactNode;
}
export const Card: React.FC<CardProps> = ({
header,
footer,
actions,
children,
}) => {
return (
<div className="card">
{header && <div className="card-header">{header}</div>}
<div className="card-body">{children}</div>
{actions && <div className="card-actions">{actions}</div>}
{footer && <div className="card-footer">{footer}</div>}
</div>
);
};This pattern scales well because each slot is intentional and optional. The component remains small—it's really just an organizational wrapper—while consumers retain full control over what goes in each region.
Compound components for related pieces
When you have a component that naturally breaks into related sub-components, the compound component pattern makes the hierarchy explicit and intuitive. A form, menu, or table often works better as a collection of related parts than as a monolith.
Imagine an order card with clearly separate concerns:
<Card>
<Card.Header title="Orders" icon={<OrderIcon />} />
<Card.Body>
{orders.map((order) => (
<OrderRow key={order.id} order={order} />
))}
</Card.Body>
<Card.Footer>
<Card.Pagination page={page} onChange={setPage} />
</Card.Footer>
</Card>The implementation uses React's composition model naturally:
interface CardProps {
children: React.ReactNode;
}
const Card: React.FC<CardProps> & {
Header: React.FC<{ title: string; icon?: React.ReactNode }>;
Body: React.FC<{ children: React.ReactNode }>;
Footer: React.FC<{ children: React.ReactNode }>;
Pagination: React.FC<{ page: number; onChange: (p: number) => void }>;
} = ({ children }) => {
return <div className="card">{children}</div>;
};
Card.Header = ({ title, icon }) => (
<div className="card-header">
{icon && <span className="icon">{icon}</span>}
<h2>{title}</h2>
</div>
);
Card.Body = ({ children }) => (
<div className="card-body">{children}</div>
);
Card.Footer = ({ children }) => (
<div className="card-footer">{children}</div>
);
Card.Pagination = ({ page, onChange }) => (
<div className="pagination">
<button onClick={() => onChange(page - 1)}>Prev</button>
<span>Page {page}</span>
<button onClick={() => onChange(page + 1)}>Next</button>
</div>
);
export default Card;This approach shines because:
- Each sub-component has a clear responsibility and minimal prop surface.
- The tree structure reflects the UI hierarchy visually, making templates easier to reason about.
- You can extend or swap sub-components without touching the parent.
- Consumers can skip sub-components they don't need (optional header? Just omit
Card.Header).
For more advanced use cases, compound components can share state via context, allowing sub-components to coordinate without explicit prop drilling.
Composition keeps components small and flexible
The overarching benefit of composition is simplicity. Small, focused components are easier to test, reason about, and reuse. They have fewer props, fewer conditional branches, and fewer hidden coupling points.
When you compose rather than configure, you:
- Reduce cognitive load. Each component does one thing; understanding it takes seconds, not minutes.
- Enable flexibility. New use cases often don't require new props—just new combinations of existing components.
- Improve testability. A small component with clear inputs and outputs is trivial to unit test.
- Encourage reuse. Because components are simple and focused, they're useful in contexts you didn't anticipate.
- Make refactoring safer. Changing a small component has smaller blast radius than changing a configuration-heavy one.
This is especially valuable as your design system grows. Rather than creating PrimaryButton, SecondaryButton, DangerButton, etc., you define Button once and let consumers compose it with icons, labels, loaders, or anything else needed.
Common mistakes
Overcomposing everything. Not every component needs sub-components. A simple Badge or Label that always displays the same way doesn't benefit from compound component structure. Save it for components with multiple distinct regions.
Passing props through slots for convenience. Don't do this:
<Card
header={<Card.Header title={data.title} isLoading={isLoading} />}
...
>It defeats the purpose. If you're always passing a Card.Header, make it a dedicated prop. Composition is for optionality and flexibility, not busywork.
Creating props that don't compose well. Props like titleStyle or contentClassName are signs that your component structure doesn't match your needs. Prefer to let consumers control the full element when they need custom styling.
Forgetting to document slot structure. When using slot props or compound components, document the expected content type and any constraints. A TypeScript interface helps, but a comment or Storybook example prevents confusion.
Key takeaways
- Replace boolean and string props with
childrenwhen you're really just controlling layout—it's simpler and more flexible. - Use slot props (
header,footer,actions) for components with multiple named content regions; it keeps the API predictable. - Reach for compound components when sub-components are naturally related and consumers benefit from explicit hierarchy.
- Composition scales better than configuration as your design system grows; prioritize it in shared components.
- Keep each component small and focused; composition makes testing, reasoning, and reuse easier.
- Watch for the smell of 15+ props—it usually means composition would serve you better.




Comments
Loading comments…