DEV Community

Joseph Salaki
Joseph Salaki

Posted on • Originally published at joemetry.com

Why Your Reusable Components Keep Breaking (And How to Fix Your API Design)

Ever stared at a component library you built just three weeks ago, only to realize it's already suffocating under a mountain of boolean props like hasBadge, isCompact, and withIcon?

I ran into this exact wall recently while refactoring a set of modular landing page cards for a mixed-media client project. What started as a clean, reusable UI module quickly devolved into a brittle spaghetti monster the moment a new layout requirement dropped. Every time a client needed a tiny structural tweak—like shifting an image from top to side, or adding a secondary action tag—I found myself cracking open the core component file and risking regressions across the entire layout.

The underlying problem isn't just poor planning; it's treating components like rigid black boxes instead of flexible composition primitives.

Here is what that trap looks like in code:

// The Trap: A monolithic component buckling under conditional props
function ProductCard({ title, price, badgeText, isLarge, hasImage, imageSrc, variant }) {
  return (
    <div className={`card ${variant} ${isLarge ? 'large' : ''}`}>
      {hasImage && <img src={imageSrc} alt={title} />}
      {badgeText && <span className="badge">{badgeText}</span>}
      <h3>{title}</h3>
      <p>{price}</p>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

To break out of this cycle, I had to shift away from monolithic prop drilling and lean into compound component patterns—handing structural control back to the consumer while keeping styles neatly encapsulated:

// The Fix: Composable layout primitives
function Card({ children, className }) {
  return <div className={`card-base ${className || ''}`}>{children}</div>;
}

Card.Header = function CardHeader({ children }) {
  return <div className="card-header">{children}</div>;
};

Card.Body = function CardBody({ children }) {
  return <div className="card-body">{children}</div>;
};

// Usage: Clean, extensible, and untouched core logic
export default function App() {
  return (
    <Card>
      <Card.Header>
        <span className="badge">Featured</span>
        <img src="/assets/preview.svg" alt="Preview" />
      </Card.Header>
      <Card.Body>
        <h3>Dynamic System Spec</h3>
        <p>Structured layout tokens in motion.</p>
      </Card.Body>
    </Card>
  );
}

Enter fullscreen mode Exit fullscreen mode

My question:

How do you usually handle this in your own codebases? Do you enforce strict, heavily-propped components to keep teams locked into a rigid design system, or have you shifted toward compound composition patterns to handle custom layout variations? How do you keep things maintainable?

Top comments (0)