C.W.K.
Stream
Lesson 03 of 07 · published

Composition Over Inheritance — The React Way

~14 min · composition, children, slot-props

Level 0React Novice
0 XP0/54 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
React has no class hierarchy you extend. You build complex UIs by nesting components inside components and passing pieces of UI as props or children. That's composition. It scales further than any class tree.

Two composition idioms

Almost all React composition reduces to two patterns:

  1. The children prop — the parent renders a frame and lets children fill the inside.
  2. Slot props — named props (other than children) that take React nodes, letting a parent compose into specific positions.

You'll see slot-prop layouts (header / body / footer) in dashboards, dialogs, and cards. The children prop alone covers the simple case; slot props win when there are multiple distinct positions.

The compound component pattern

Sometimes you want callers to write <Tabs><Tab>A</Tab><Tab>B</Tab></Tabs>. The Tabs parent owns the active state, and Tab children read it from context. This is compound components — parent and children share an implicit state contract. Use sparingly; great for libraries, can be over-engineered for app code.

The asChild / Slot pattern

A pattern Radix UI popularized: instead of cloning a styled <button> into a <Link>, you let callers pass asChild and the component renders its children as the actual element, merging in its own behavior. Lets the same Button become a real anchor when wrapped around a <Link>.

If you reach for a higher-order component, stop. Composition is almost always cleaner. The cases where HOCs win are vanishingly rare in 2026 — usually data-fetching wrappers from libraries (which themselves use hooks now). Lesson 6 of this track is the death notice.

Code

children — the simple case·tsx
function Card({ children }: { children: React.ReactNode }) {
  return (
    <article className="rounded-card bg-bg-elevated p-6 shadow-sm">
      {children}
    </article>
  );
}

// Caller fills the inside:
<Card>
  <h2>Hello</h2>
  <p>This is a card.</p>
</Card>
Slot props — named positions·tsx
type DialogProps = {
  title: React.ReactNode;
  body: React.ReactNode;
  actions?: React.ReactNode;
};

function Dialog({ title, body, actions }: DialogProps) {
  return (
    <div role="dialog" className="rounded-card bg-bg-elevated p-6 space-y-4">
      <header className="font-semibold text-fg">{title}</header>
      <div>{body}</div>
      {actions && (
        <footer className="flex justify-end gap-2">{actions}</footer>
      )}
    </div>
  );
}

// Caller composes into named positions:
<Dialog
  title="Delete conversation?"
  body={<p>This cannot be undone.</p>}
  actions={
    <>
      <button onClick={cancel}>Cancel</button>
      <button onClick={confirm}>Delete</button>
    </>
  }
/>
Compound components — shared implicit state·tsx
import { createContext, useContext, useState } from "react";

const TabsCtx = createContext<{
  active: string;
  setActive: (id: string) => void;
} | null>(null);

function Tabs({ defaultActive, children }: {
  defaultActive: string;
  children: React.ReactNode;
}) {
  const [active, setActive] = useState(defaultActive);
  return (
    <TabsCtx.Provider value={{ active, setActive }}>
      <div className="flex gap-2 border-b">{children}</div>
    </TabsCtx.Provider>
  );
}

function Tab({ id, children }: { id: string; children: React.ReactNode }) {
  const ctx = useContext(TabsCtx);
  if (!ctx) throw new Error("<Tab> must be inside <Tabs>");
  const isActive = ctx.active === id;
  return (
    <button
      onClick={() => ctx.setActive(id)}
      className={isActive ? "text-brand border-b-2 border-brand" : "text-muted"}
    >
      {children}
    </button>
  );
}

// Caller uses both together; the state is hidden:
<Tabs defaultActive="a">
  <Tab id="a">First</Tab>
  <Tab id="b">Second</Tab>
</Tabs>

External links

Exercise

Build a Modal component with three slot props: header, body, footer. Then build a sibling Modal2 that uses the compound-component pattern with Modal2.Header, Modal2.Body, Modal2.Footer children. Use both to render the same delete-confirmation UI. Notice which one feels more natural to read at the call site, and which one is easier to define.
Hint
For the compound pattern: Modal2.Header = function Header(...) after defining Modal2. The children inspect their position via context or a marker prop.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.