Learn

/

Children Pattern

Children Pattern

6 patterns

Composition, compound components, named slots, and the slots/slotProps convention.

Generic list component
hard
Avoid
interface ListProps<T> {
  /** The data to iterate. */
  data: T[];
  /** Render function for each item. */
  render: (item: T) => React.ReactNode;
  /** Empty state content. */
  empty: React.ReactNode;
}

Prefer
interface ListProps<T> {
  /** The data array to iterate over. */
  data: T[];
  /**
   * Render function called for each item.
   * Receives the item and its index.
   */
  renderItem: (item: T, index: number) => React.ReactNode;
  /**
   * Content shown when the data array is empty.
   * @example
   * emptyState={<EmptyMessage text="No items" />}
   */
  emptyState: React.ReactNode;
}

renderItem is specific (render what? an item), the callback includes index for key assignment and alternating styles, and emptyState clearly describes the slot's purpose.

The @example in JSDoc shows the expected usage pattern. Compare render (of what?) and empty (a boolean? a message?).

TypeScript: Generics
Compound component API design
hard
Avoid
interface SelectProps {
  options: Array<{
    value: string;
    label: string;
    disabled?: boolean;
    icon?: React.ReactNode;
    group?: string;
  }>;
  value: string;
  onChange: (value: string) => void;
  placeholder?: string;
}

Prefer
interface SelectOptionProps {
  /** The value submitted when selected. */
  value: string;
  /** Display label. Falls back to children. */
  children: React.ReactNode;
  /** Whether this option is non-selectable. */
  isDisabled?: boolean;
}

interface SelectProps {
  /** Compose with SelectOption components. */
  children: React.ReactNode;
  /** The currently selected value. */
  value: string;
  /** Called when the user selects an option. */
  onValueChange: (value: string) => void;
  /** Text shown when no value is selected. */
  placeholder?: string;
}

Compound components (Select + SelectOption) are more flexible than config objects. They support nesting, conditional rendering, custom content, and compose naturally with JSX. Adding icons or groups doesn't require changing the config type.

React Docs: Thinking in React
Customizing sub-components
medium
Avoid
interface AutocompleteProps {
  options: string[];
  PaperComponent?: React.ElementType;
  PaperProps?: Record<string, unknown>;
  ListboxComponent?: React.ElementType;
  ListboxProps?: Record<string, unknown>;
  PopperComponent?: React.ElementType;
  PopperProps?: Record<string, unknown>;
}

Prefer
interface AutocompleteProps {
  options: string[];
  /**
   * Override internal sub-components and their props.
   * Each slot maps to an internal element.
   */
  slots?: {
    paper?: React.ElementType;
    listbox?: React.ElementType;
    popper?: React.ElementType;
  };
  slotProps?: {
    paper?: Record<string, unknown>;
    listbox?: Record<string, unknown>;
    popper?: Record<string, unknown>;
  };
}

Grouping overrides into slots and slotProps objects scales cleanly. Adding a new customizable element means adding one key to each object — not two new top-level props.

This is the pattern MUI adopted across all components, replacing the older PaperComponent/PaperProps pairs that cluttered the API.

MUI: Themed Components
Named content slots
medium
Avoid
interface CardProps {
  title: string;
  subtitle?: string;
  iconName?: string;
  actions?: string[];
  children: React.ReactNode;
  footerText?: string;
}

Prefer
interface CardProps {
  children: React.ReactNode;
  /** Content rendered in the card header. */
  header?: React.ReactNode;
  /** Action buttons rendered in the footer. */
  footer?: React.ReactNode;
  /** Icon shown before the header content. */
  icon?: React.ReactNode;
}

Slot props (header, footer, icon as ReactNode) let consumers pass anything — text, icons, buttons, or custom components. String-typed slots restrict you to plain text; ReactNode slots enable full composition.

React Docs: Passing Props
Primary content as children
easy
Avoid
interface PanelProps {
  title: string;
  content: React.ReactNode;
}

// Usage:
// <Panel
//   title="Settings"
//   content={<SettingsForm />}
// />

Prefer
interface PanelProps {
  title: string;
  children: React.ReactNode;
}

// Usage:
// <Panel title="Settings">
//   <SettingsForm />
// </Panel>

The primary content of a component should use children, not a named prop. This follows JSX composition conventions and lets consumers nest content naturally. Named slots like header or footer are for secondary content areas.

React Docs: Passing JSX as children
Composition over prop drilling
easy
Avoid
interface LayoutProps {
  userName: string;
  userAvatar: string;
  userRole: string;
  navItems: NavItem[];
  children: React.ReactNode;
}

Prefer
interface LayoutProps {
  /** Rendered in the sidebar area. */
  sidebar: React.ReactNode;
  /** Rendered in the top header area. */
  header: React.ReactNode;
  /** Main content area. */
  children: React.ReactNode;
}

Passing pre-rendered ReactNode slots avoids prop drilling. The Layout doesn't need to know about user data or nav items — it just renders whatever components the parent composes. If the sidebar design changes, only the parent's JSX changes, not the Layout props.

React Docs: Passing JSX as children