Children and Compound Components

Introduction

children lets parents pass arbitrary JSX into a component—similar to Vue’s default slot. Compound components split layout into named parts (Card, Card.Header, Card.Body) without prop explosion. This chapter covers composition patterns that replace Vue slots.

Prerequisites

children Prop

Card.tsx:

Usage:

tsx
<Card title="Settings">
  <p>Notification preferences go here.</p>
  <button type="button">Save</button>
</Card>

Code explanation:

  • Content between opening and closing tags becomes children
  • React.ReactNode accepts strings, elements, fragments, arrays

Compare Vue <slot> in Slots.

Named Regions via Props

Slot-like API without context:

tsx
<PageLayout
  header={<h1>Dashboard</h1>}
  sidebar={<Nav />}
>
  <DashboardContent />
</PageLayout>

Render Props (Pattern)

Pass a function that returns UI:

tsx
type ListProps<T> = {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
};
 
export function List<T>({ items, renderItem }: ListProps<T>) {
  return <ul>{items.map((item) => <li key={String(item)}>{renderItem(item)}</li>)}</ul>;
}
tsx
<List
  items={users}
  renderItem={(user) => <span>{user.name}</span>}
/>

Use when the parent needs flexible markup; custom hooks often replace render props today.

Compound Components

Tabs.tsx:

Usage:

tsx
<Tabs defaultTab="profile">
  <Tabs.List>
    <Tabs.Tab id="profile">Profile</Tabs.Tab>
    <Tabs.Tab id="billing">Billing</Tabs.Tab>
  </Tabs.List>
  <Tabs.Panel id="profile">Profile form</Tabs.Panel>
  <Tabs.Panel id="billing">Billing info</Tabs.Panel>
</Tabs>

Compound components share implicit state via Context—next chapter goes deeper.

When to Split Components

SmellFix
Huge JSX blockExtract Header, Footer
Repeated layoutPageLayout with children
Flexible insertion pointsNamed props or compound components

FAQ

children vs props slot?

children is the default slot; extra ReactNode props are named slots.

Empty children?

children can be undefined—render {children ?? null} if needed.

Fragment as children?

Valid—parent receives multiple nodes without a wrapper.

Scoped slot equivalent?

Render prop renderItem={(item) => ...} or pass component as prop.

Next chapter?

Context API.