Component Composition
How components combine and communicate — children props, slot patterns, and why composition beats deep prop drilling.
Principle
Prop drilling happens when you pass data through multiple components that do not use it — just to get it to a component deep in the tree. Composition solves this differently: instead of passing data down, you pass components down. The parent controls what gets rendered, and children receive exactly what they need directly.
When you find yourself adding a prop to a component just to pass it further down, stop. That is the signal to use composition instead.
Rules
- check_circleUse children for flexible contentThe children prop lets a parent inject content into a component without the component needing to know what it is.
- check_circleUse named slots for multiple injection pointsWhen you need more than one place to inject content (header + footer + body), use named props instead of children.
- check_circlePrefer composition over configurationA component that accepts children is more flexible than one with 10 props controlling its internals. Compose behavior, do not configure it.
- check_circleKeep components focusedEach component does one thing. Composition is how you build complex UIs from simple, focused pieces.
Pattern
// ❌ Prop drilling — Card needs to know about title, footer, etc. <Card title="Recipe" subtitle="Foundations" footer={<Button>View</Button>} headerIcon="layers" /> // ✅ Composition — Card just provides structure <Card> <Card.Header> <span>Foundations</span> <h2>Recipe</h2> </Card.Header> <Card.Body> Content goes here </Card.Body> <Card.Footer> <Button>View</Button> </Card.Footer> </Card> // The Card implementation interface CardProps { children: React.ReactNode } interface CardHeaderProps { children: React.ReactNode } function Card({ children }: CardProps) { return <div className="rounded-xl border bg-white">{children}</div>; } function CardHeader({ children }: CardHeaderProps) { return <div className="p-4 border-b">{children}</div>; } Card.Header = CardHeader; Card.Body = ({ children }: CardProps) => <div className="p-4">{children}</div>; Card.Footer = ({ children }: CardProps) => <div className="p-4 border-t">{children}</div>;
Implementation
Version Compatibility
Requires React 19+ and the latest stable versions of all dependencies shown.
In Next.js, composition works the same way. Server Components can pass Client Components as children — this is how you keep server/client boundaries clean.
// Server Component — fetches data export default async function RecipePage({ params }: PageProps) { const detail = await getRecipeDetail(params.slug); return ( // Passes a Client Component as children <RecipeLayout header={<RecipeHeader title={detail.title} />} sidebar={<RecipeToc sections={detail.sections} />} > {/* Client Component receives data as props, not fetching itself */} <RecipeContent detail={detail} /> </RecipeLayout> ); } // RecipeLayout — just structure, no data concerns interface RecipeLayoutProps { header: React.ReactNode; sidebar: React.ReactNode; children: React.ReactNode; } export function RecipeLayout({ header, sidebar, children }: RecipeLayoutProps) { return ( <div> <header>{header}</header> <div className="flex"> <aside>{sidebar}</aside> <main>{children}</main> </div> </div> ); }