A button component with forty props can render almost anything, and that's the trouble with it. The components I trust make one product decision explicit and refuse the rest.
Take a button. It carries hierarchy, disabled and loading states, icon alignment, a focus ring, and whether the action destroys something. A list card decides how much a row can say before the user needs a detail page. Those are product calls, and the component is where your team writes them down.
Reuse comes second
Teams that start with reuse as the goal end up with prop-heavy components that can look like anything. Start with the boundary, and answer a few questions before you write JSX:
- Which decision does this component own?
- Which states are valid, and which should the types make impossible?
- What data shape does it need, and what interaction does it promise?
Once you can answer those, you'll spot the second and third call sites without hunting for them.
Prefer named variants to boolean flags
Booleans feel fast. Five of them give you thirty-two combinations, and nobody on the team can tell you which ones the design supports.
type ProjectCardTone = "featured" | "standard" | "quiet";
type ProjectCardProps = {
tone: ProjectCardTone;
title: string;
summary: string;
tags: string[];
};
Three named tones tell the next engineer what the design system supports, and TypeScript rejects a fourth until someone adds it on purpose.
Boundaries make review concrete
With a clear boundary, you review a pull request against the props instead of taste. You ask whether the new state belongs to this component, whether the data model still fits, and whether the hierarchy still matches the job the user came to do. You can answer each of those by reading the props.
Volodymyr Chornous

