React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

compositionslotschildrenlayoutreact-patterns

Composition Over Inheritance — Build flexible UIs by composing components instead of extending them

Recipe

// 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.

Working Example

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:

  • Slot pattern via named ReactNode props (media, actions, children)
  • Specialization by wrapping a generic component (Card) inside a domain-specific one (ProductCard)
  • Zero inheritance — ProductCard is not a subclass of Card

Deep Dive

How It Works

  • React components are functions that return JSX. There is no class hierarchy to extend.
  • children is the primary composition primitive — any JSX nested inside a component is passed as props.children.
  • Named slots (props typed as ReactNode) let you inject content into specific positions within a component's layout.
  • Specialization is achieved by creating a new component that renders a general component with specific props pre-filled.
  • Containment is achieved by components that don't know their children ahead of time and use {children} as a placeholder.

Parameters & Return Values

Prop PatternTypePurpose
childrenReactNodeDefault slot for nested content
Named slot (e.g. header)ReactNodeExplicit placement of content in a specific area
Render callback(data: T) => ReactNodeContent that needs access to internal state (see render-props)
Component propReact.ComponentType<P>Inject a full component to be instantiated internally

Variations

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>
  );
}

TypeScript Notes

  • Use ReactNode for slots that accept any renderable content (strings, elements, fragments, null).
  • Use React.ComponentType<P> when you need to pass a component that will be instantiated with specific props.
  • Use ReactElement only when you need to narrow to actual JSX elements (excluding strings and numbers).
  • Generic composition (List<T>) preserves type safety through the component boundary.

Gotchas

  • 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.

Alternatives

ApproachTrade-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 propsMore power when slot content needs internal state
Higher-order componentsAdds behavior without changing API, but harder to debug
InheritanceNot recommended in React — breaks with function components

FAQs

What is the difference between composition and inheritance in React?
  • React components are functions, not classes, so there is no class hierarchy to extend.
  • Composition means building UIs by nesting and combining components instead of creating subclass relationships.
  • Specialization is achieved by wrapping a general component with specific props pre-filled, not by extending it.
  • React officially recommends composition over inheritance in every case.
When should you use named slot props instead of just children?
  • Use named ReactNode props (like header, sidebar, actions) when content needs to be placed in specific positions within a layout.
  • Use children alone only when there is a single content area.
  • Named slots give you explicit control over placement that children alone cannot provide.
How does the component injection pattern differ from passing rendered elements as props?
// Rendered element: you pass JSX
<Card media={<img src={url} alt="photo" />} />
 
// Component injection: you pass a component type
<List items={data} renderItem={ProductRow} />
  • With component injection, the parent component instantiates the injected component internally and controls what props it receives.
  • With rendered elements, the consumer controls instantiation and prop values.
How do you flatten deeply nested context providers using composition?
function AppProviders({ children }: { children: ReactNode }) {
  return (
    <ThemeProvider>
      <AuthProvider>
        <QueryProvider>
          {children}
        </QueryProvider>
      </AuthProvider>
    </ThemeProvider>
  );
}
  • Create a single wrapper component that nests all providers.
  • Consumers only see <AppProviders> at the call site.
What is the difference between ReactNode, ReactElement, and React.ComponentType<P> in TypeScript?
  • 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.
How does generic composition like List<T> preserve type safety?
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>;
}
  • TypeScript infers T from the items array and enforces it on renderItem.
  • The consumer gets full autocomplete on the item prop inside the render component.
Gotcha: Why does passing inline JSX as a slot prop break memoization?
  • Passing <Component /> inline as a prop creates a new React element reference every render.
  • If the receiving component uses React.memo, the new reference causes it to re-render.
  • Fix: lift static slot content outside the render function or wrap with useMemo.
Gotcha: When does creating a wrapper component become an anti-pattern?
  • If your wrapper re-exposes all original props without adding behavior, you have an unnecessary layer.
  • This is configuration, not composition. Just use the original component directly and pass props at the call site.
  • Wrappers are appropriate when they pre-fill specific props to create a domain-specific specialization.
Why is inheritance not recommended in React?
  • React's component model is based on functions and composition, not class hierarchies.
  • Function components cannot be extended via inheritance at all.
  • Even with class components, React's team has never found a use case where inheritance is preferable to composition.
When should you use a render callback prop instead of a ReactNode slot?
  • Use a render callback ((data: T) => ReactNode) when the slot content needs access to internal state from the parent component.
  • Use a plain ReactNode slot when the content is independent of the component's internal state.
  • Render callbacks add flexibility but also complexity. Start with ReactNode and upgrade only when needed.
How do you type a component that accepts both children and named slots in TypeScript?
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>
  );
}
  • All slot props are typed as ReactNode.
  • children is a standard React prop that receives nested JSX.