Component Composition — Building UIs from Small Pieces

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

🧠 Test Yourself

You need to add a “Sponsored” badge to some post cards. The badge appears in the same position as the status badge. With good component composition, how many components need to change?