Back to all notes

Compound Components: Flexible APIs Built from Small Pieces

Create components that work together via context so consumers can compose them in any order.

2 min read

The first PostCard I wrote decided everything itself: title, then content, then author, always that order. If a consumer wanted the author first, the component had to change. Hardcoding section order or threading props between siblings both lead to rigid, brittle APIs.

Compound components are the other road. You build a group of components meant to work together, like Tabs, Tabs.List, and Tabs.Panel, and they share context. The consumer composes them in whatever order it needs. Tabs, cards, dropdowns, anything with slots worth arranging, work well this way.

The monolithic PostCard

One component controlling both layout and content:

function PostCard({ post }) {
  return (
    <div className="post-card">
      <h2>{post.title}</h2>
      <p>{post.content}</p>
      <p>— {post.author}</p>
    </div>
  );
}

function App() {
  const post = {
    title: "Understanding React Patterns",
    content: "React patterns help us write better, more maintainable code...",
    author: "John Doe"
  };
  return (
    <div>
      <h1>Compound Components Pattern</h1>
      <PostCard post={post} />
    </div>
  );
}

export default App;

Splitting PostCard with context

Share the data through context and expose the sub-components as static properties on the parent:

import { createContext, useContext } from "react";

const PostContext = createContext();
const usePost = () => useContext(PostContext);

function PostCard({ children, post }) {
  return (
    <PostContext.Provider value={post}>
      <div className="post-card">{children}</div>
    </PostContext.Provider>
  );
}

PostCard.Header = function PostCardHeader() {
  const post = usePost();
  return <h2>{post.title}</h2>;
};

PostCard.Body = function PostCardBody() {
  const post = usePost();
  return <p>{post.content}</p>;
};

PostCard.Footer = function PostCardFooter() {
  const post = usePost();
  return <p>— {post.author}</p>;
};

function App() {
  const post = {
    title: "Understanding React Patterns",
    content: "React patterns help us write better, more maintainable code...",
    author: "John Doe"
  };
  return (
    <div>
      <h1>Compound Components Pattern</h1>
      <PostCard post={post}>
        <PostCard.Header />
        <PostCard.Body />
        <PostCard.Footer />
      </PostCard>
    </div>
  );
}

export default App;

PostCard wraps its children in a provider holding the post. PostCard.Header, PostCard.Body, and PostCard.Footer read it back through a small usePost helper, so the consumer can list them in any order. The sub-components themselves only render; state doesn’t belong in them.

Document which sub-components exist and whether any order is required, ideally none is. For controlled versus uncontrolled behavior, expose both a stateful and a stateless variant.