Search across all documentation pages
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
// Slot-based composition: pass components as props
function Layout({ header, sidebar, children }: {
header: React.ReactNode;
sidebar: React.ReactNode;
children: React.ReactNode;
}) {
return (
<div className="grid grid-cols-[240px_1fr] grid-rows-[64px_1fr]">
<header className="col-span-2">{header}</header>
<aside>{sidebar}</aside>
<main>{children}</main>
</div>
);
}
// Usage
<Layout
header={<TopNav user={currentUser} />}
sidebar={<SideMenu items={menuItems} />}
>
<Dashboard data={dashboardData} />
</Layout>When to reach for this: Whenever you need a reusable container, layout, or wrapper that should remain agnostic about its content. Prefer composition over inheritance in every case in React.
import { type ReactNode } from "react";
// A Card component with composable slots
interface CardProps {
media?: ReactNode;
actions?: ReactNode;
children: ReactNode;
variant?: "elevated" | "outlined";
}
function Card({ media, actions, children, variant = "outlined" }: CardProps) {
const base = "rounded-lg overflow-hidden";
const styles = variant === "elevated"
? `${base} shadow-lg bg-white`
: `${base} border border-gray-200 bg-white`;
return (
<div className={styles}>
{media && <div className="aspect-video overflow-hidden">{media}</div>}
<div className="p-4">{children}</div>
{actions && (
<div className="px-4 pb-4 flex gap-2 justify-end">{actions}</div>
)}
</div>
);
}
// Specialization through composition, NOT inheritance
function ProductCard({ product }: { product: Product }) {
return (
<Card
variant="elevated"
media={<img src={product.image} alt={product.name} />}
actions={
<>
<button className="btn-secondary">Save</button>
<button className="btn-primary">Add to Cart</button>
</>
}
>
<h3 className="font-semibold">{product.name}</h3>
<p className="text-gray-600">${product.price}</p>
</Card>
);
}
interface Product {
name: string;
price: number;
image: string;
}What this demonstrates:
ReactNode props (media, actions, children)Card) inside a domain-specific one (ProductCard)ProductCard is not a subclass of Cardchildren is the primary composition primitive - any JSX nested inside a component is passed as props.children.ReactNode) let you inject content into specific positions within a component's layout.{children} as a placeholder.| Prop Pattern | Type | Purpose |
|---|---|---|
children | ReactNode | Default slot for nested content |
Named slot (e.g. header) | ReactNode | Explicit placement of content in a specific area |
| Render callback | (data: T) => ReactNode | Content that needs access to internal state (see render-props) |
| Component prop | React.ComponentType<P> | Inject a full component to be instantiated internally |
Component injection pattern - pass a component type rather than a rendered element:
interface ListProps<T> {
items: T[];
renderItem: React.ComponentType<{ item: T }>;
}
function List<T>({ items, renderItem: Item }: ListProps<T>) {
return (
<ul>
{items.map((item, i) => (
<li key={i}><Item item={item} /></li>
))}
</ul>
);
}Provider composition - flatten nested context providers:
function AppProviders({ children }: { children: ReactNode }) {
return (
<ThemeProvider>
<AuthProvider>
<QueryProvider>
{children}
</QueryProvider>
</AuthProvider>
</ThemeProvider>
);
}ReactNode for slots that accept any renderable content (strings, elements, fragments, null).React.ComponentType<P> when you need to pass a component that will be instantiated with specific props.ReactElement only when you need to narrow to actual JSX elements (excluding strings and numbers).List<T>) preserves type safety through the component boundary.Overusing children for multiple slots - When you pass everything as children, you lose control over placement. Fix: Use named ReactNode props for distinct content areas.
Prop drilling through composed layers - Deep composition can lead to passing props through many levels. Fix: Use context for truly cross-cutting concerns, and keep composition shallow.
Breaking memoization with inline JSX slots - Passing <Component /> inline as a prop creates a new element reference each render. Fix: Lift static slot content outside the render or wrap with useMemo for expensive trees.
Confusing specialization with configuration - Creating a new component just to set a few props is fine; creating a wrapper that re-exposes all original props is a sign you need composition, not wrapping. Fix: Use the original component directly and pass props at the call site.
| Approach | Trade-off |
|---|---|
| Composition (slots) | Most flexible; requires more JSX at the call site |
| Configuration props (variant strings) | Less flexible but simpler API for common cases |
| Render props | More power when slot content needs internal state |
| Higher-order components | Adds behavior without changing API, but harder to debug |
| Inheritance | Not recommended in React - breaks with function components |
ReactNode props (like header, sidebar, actions) when content needs to be placed in specific positions within a layout.children alone only when there is a single content area.children alone cannot provide.// Rendered element: you pass JSX
<Card media={<img src={url} alt="photo" />} />
// Component injection: you pass a component type
<List items={data} renderItem={ProductRow} />function AppProviders({ children }: { children: ReactNode }) {
return (
<ThemeProvider>
<AuthProvider>
<QueryProvider>
{children}
</QueryProvider>
</AuthProvider>
</ThemeProvider>
);
}<AppProviders> at the call site.ReactNode accepts any renderable content: strings, numbers, elements, fragments, and null.ReactElement narrows to actual JSX elements only, excluding strings and numbers.React.ComponentType<P> is a component type (function or class) that can be instantiated with props of type P.interface ListProps<T> {
items: T[];
renderItem: React.ComponentType<{ item: T }>;
}
function List<T>({ items, renderItem: Item }: ListProps<T>) {
return <ul>{items.map((item, i) => <li key={i}><Item item={item} /></li>)}</ul>;
}T from the items array and enforces it on renderItem.item prop inside the render component.<Component /> inline as a prop creates a new React element reference every render.React.memo, the new reference causes it to re-render.useMemo.(data: T) => ReactNode) when the slot content needs access to internal state from the parent component.ReactNode slot when the content is independent of the component's internal state.ReactNode and upgrade only when needed.interface LayoutProps {
header: ReactNode;
sidebar: ReactNode;
children: ReactNode;
}
function Layout({ header, sidebar, children }: LayoutProps) {
return (
<div>
<header>{header}</header>
<aside>{sidebar}</aside>
<main>{children}</main>
</div>
);
}ReactNode.children is a standard React prop that receives nested JSX.Reviewed by Chris St. John·Last updated Jul 16, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥