Component composition is the idea that complex UIs are built by assembling simpler components together, like LEGO bricks. A PostCard does not implement an avatar from scratch — it renders an Avatar component. An Avatar does not implement a badge from scratch — it uses the Badge component for the “online” indicator. This hierarchy of small, focused components is React’s primary design strategy: each component does one thing well, is easy to test in isolation, and can be reused anywhere in the application.
Composing the PostCard Hierarchy
// ── Level 1: Primitive components ─────────────────────────────────────────────
function Avatar({ src, name, size = 36 }) {
return (
<img
src={src || `https://ui-avatars.com/api/?name=${encodeURIComponent(name)}&size=${size}`}
alt={name}
width={size}
height={size}
className="rounded-full object-cover"
/>
);
}
function Badge({ children, variant = "default" }) {
const styles = {
default: "bg-gray-100 text-gray-600",
published: "bg-green-100 text-green-700",
draft: "bg-yellow-100 text-yellow-700",
tag: "bg-blue-100 text-blue-700",
};
return (
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${styles[variant]}`}>
{children}
</span>
);
}
// ── Level 2: Compound components built from primitives ────────────────────────
function AuthorLine({ author, publishedAt }) {
const date = new Date(publishedAt).toLocaleDateString("en-US", {
month: "short", day: "numeric", year: "numeric",
});
return (
<div className="flex items-center gap-2 text-sm text-gray-500">
<Avatar src={author.avatar_url} name={author.name} size={24} />
<span>{author.name}</span>
<span>·</span>
<time>{date}</time>
</div>
);
}
function TagList({ tags }) {
if (!tags?.length) return null;
return (
<div className="flex flex-wrap gap-1 mt-2">
{tags.map((tag) => (
<Badge key={tag.id} variant="tag">{tag.name}</Badge>
))}
</div>
);
}
// ── Level 3: Feature component assembling everything ──────────────────────────
function PostCard({ post }) {
return (
<article className="bg-white rounded-xl shadow-sm border border-gray-100 p-5 hover:shadow-md transition-shadow">
<div className="flex items-start justify-between mb-2">
<Badge variant={post.status}>{post.status}</Badge>
<span className="text-sm text-gray-400">{post.view_count} views</span>
</div>
<h2 className="text-lg font-semibold text-gray-900 mb-1 line-clamp-2">
{post.title}
</h2>
<p className="text-gray-500 text-sm line-clamp-3 mb-3">{post.body}</p>
<TagList tags={post.tags} />
<div className="mt-4 pt-3 border-t border-gray-50">
<AuthorLine author={post.author} publishedAt={post.created_at} />
</div>
</article>
);
}
Note: The composition hierarchy — primitive → compound → feature → page — gives each component level a clear responsibility. Primitives (
Avatar, Badge) have no domain knowledge; they only handle presentation. Compound components (AuthorLine, TagList) combine primitives with a specific semantic purpose. Feature components (PostCard) assemble compounds and know about the domain (a “post” with its “author” and “tags”). Page components fetch data and lay out feature components.Tip: Design components to accept
className as a prop and merge it with the component’s own classes. This lets consumers override styles without forking the component: function Card({ className = "", children }) { return <div className={`bg-white rounded p-4 ${className}`}>{children}</div>; }. Then <Card className="mt-8"> adds top margin without changing the base card styles. This pattern is used extensively in popular component libraries like shadcn/ui.Warning: Resist the urge to build one massive component that handles everything. A
PostCard that fetches its own data, manages its own like state, handles deletion, and formats dates will become unmaintainable. Follow the single responsibility principle: PostCard renders a post given as a prop; a separate PostCardContainer or page-level component handles data fetching and state management. This separation makes PostCard trivially testable with any mock data.Flattening vs Nesting — Prop Drilling
// ── Prop drilling problem — passing data through many levels ──────────────────
// ❌ Wrong pattern — currentUser passed through every level just to reach LikeButton
function PostFeed({ posts, currentUser }) {
return posts.map(post => <PostCard key={post.id} post={post} currentUser={currentUser} />);
}
function PostCard({ post, currentUser }) {
return <div>...<LikeButton postId={post.id} currentUser={currentUser} /></div>;
}
function LikeButton({ postId, currentUser }) {
// currentUser finally used here — drilled through two levels
}
// ✓ Better for small apps — context or state store (covered in Chapter 39)
// For now, keep component trees shallow and pass only what each component needs
Common Mistakes
Mistake 1 — One giant component instead of composition
❌ Wrong — 200-line PostCard that does everything:
function PostCard({ post }) {
// 200 lines: fetch data, format dates, render avatar, tags, likes, delete button...
// Impossible to reuse parts, hard to test, hard to read
}
✅ Correct — compose small focused components.
Mistake 2 — Not accepting className for flexible styling
❌ Wrong — card style cannot be overridden by consumer:
function Card({ children }) {
return <div className="bg-white p-4">{children}</div>; // no className prop
}
✅ Correct:
function Card({ children, className = "" }) {
return <div className={`bg-white p-4 ${className}`}>{children}</div>; // ✓
}
Quick Reference
| Level | Example | Responsibility |
|---|---|---|
| Primitive | Avatar, Badge, Button |
Pure presentation, no domain logic |
| Compound | AuthorLine, TagList |
Combines primitives, semantic meaning |
| Feature | PostCard, CommentItem |
Assembles compounds, knows domain |
| Page | HomePage, PostDetailPage |
Fetches data, lays out features |