Children Pattern
Composition, compound components, named slots, and the slots/slotProps convention.
Generic list component
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;
}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?).
Compound component API design
interface SelectProps {
options: Array<{
value: string;
label: string;
disabled?: boolean;
icon?: React.ReactNode;
group?: string;
}>;
value: string;
onChange: (value: string) => void;
placeholder?: string;
}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.
Customizing sub-components
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>;
}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.
Named content slots
interface CardProps {
title: string;
subtitle?: string;
iconName?: string;
actions?: string[];
children: React.ReactNode;
footerText?: string;
}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.
Primary content as children
interface PanelProps {
title: string;
content: React.ReactNode;
}
// Usage:
// <Panel
// title="Settings"
// content={<SettingsForm />}
// />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.
Composition over prop drilling
interface LayoutProps {
userName: string;
userAvatar: string;
userRole: string;
navItems: NavItem[];
children: React.ReactNode;
}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.