Composition beats configuration when a component grows props
Watch a component age and you will see the same pattern: it starts with two props,
then grows showIcon, iconPosition, hideFooter, variant, dense,
withDivider — a boolean for every variation anyone ever needed. Each addition is
individually reasonable and the sum is a component nobody can use without reading
its source. This is the configuration trap, and the way out is almost never
another prop. It is composition: instead of a flag that toggles a piece,
expose the pieces and let the caller assemble them. Configuration scales to a
handful of variations; composition scales to variations you never anticipated.
The configuration version collapses under its own props
Here is the component two years in. Every branch is a variation someone needed, and the caller has to know all of them to predict what renders:
// the caller can't tell what this renders without reading Card's source
function Card({ title, body, showIcon, icon, hideFooter, footerText, dense, variant }) {
return (
<div className={`card card--${variant} ${dense ? "card--dense" : ""}`}>
<h3>{showIcon && <Icon name={icon} />}{title}</h3>
<p>{body}</p>
{!hideFooter && <footer>{footerText}</footer>}
</div>
);
}
Adding one more variation means one more prop and one more branch — forever.
The composition version hands over the pieces
Expose the structure as sub-components (or children) and let the caller decide
what goes where. The Card stops deciding and starts containing:
// Card provides structure; the caller composes the content it wants
function Card({ children }) { return <div className="card">{children}</div>; }
Card.Header = ({ children }) => <h3 className="card__header">{children}</h3>;
Card.Body = ({ children }) => <div className="card__body">{children}</div>;
Card.Footer = ({ children }) => <footer className="card__footer">{children}</footer>;
// the caller assembles exactly what THIS card needs — no flags, no source-diving
<Card>
<Card.Header><Icon name="star" /> Featured</Card.Header>
<Card.Body>{body}</Card.Body>
{/* no footer? just don't include one — no `hideFooter` prop needed */}
</Card>
“No footer” is expressed by not writing one, not by a hideFooter={true}. A new
variation is a new arrangement of existing pieces, requiring no change to Card at
all.
Configuration still wins for the constrained cases
This is a bias, not an absolute. Configuration is right when the set of variations
is small, closed, and you want to constrain callers — a Button with
variant="primary" | "secondary" should not let callers compose arbitrary
internals, because the whole point is consistency. Composition is right when the
variations are open-ended and callers legitimately need to arrange the parts:
layouts, cards, lists, tables, anything “container-like.” The tell that you have
picked wrong is a boolean prop that only exists to hide or show a chunk of markup —
that is composition asking to be let out. The tabs-molecule and form-field-molecule
exercises are good places to feel the line: a tab set is naturally composed
(arbitrary panels), while a form field is naturally configured (a fixed
label/input/error shape). Choosing correctly per component is the API-design skill
the whole atomic-design vocabulary is in service of.