Skip to the content.

Composition beats configuration when a component grows props

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.

A configuration component grows props; a composition component exposes slots Left: one Card component with a growing list of boolean props. Right: a Card exposing Header, Body and Footer slots the caller composes freely. configuration <Card showIconhideFooterdensewithDividervariant… composition Card.HeaderCard.BodyCard.Footer
Configuration answers every variation with a flag inside the component; composition hands the caller the pieces and gets out of the way.

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.