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 (1)

Collapse
 
alexandersstudi profile image
Alexander •

The boolean prop trap is a classic anti-pattern that destroys maintainability as a component library scales. Moving to compound components and leveraging slot-based composition is exactly the right fix to untangle that monolithic code. I read a book called Ship Your Design System on Amazon recently, and the component API chapter breaks down this exact transition away from rigid black boxes. How are you handling validation to ensure devs only pass the intended child elements into those wrappers?