Slot-based architecture is a powerful pattern for creating highly flexible and reusable components by allowing parent components to inject content into specific “slots” within child components.
Core Concepts
Slot-based components provide named insertion points where consumers can place their own content:
- Named Slots: Designated areas for content injection
- Default Content: Fallback content when slots aren’t provided
- Scoped Slots: Slots that receive data from the child component
- Composition Control: Parent components control what renders where
Basic Implementation
1. Using children
and props
function Card({ header, footer, children }) {
return (
<div className="card">
{header && <div className="card-header">{header}</div>}
<div className="card-body">{children}</div>
{footer && <div className="card-footer">{footer}</div>}
</div>
);
}
// Usage
<Card
header={<h2>Card Title</h2>}
footer={<button>Save</button>}
>
<p>Card content goes here</p>
</Card>
2. Using React’s children
Utilities
function Layout({ children }) {
const header = Children.toArray(children).find(
child => child.type === Layout.Header
);
const content = Children.toArray(children).find(
child => child.type === Layout.Content
);
const footer = Children.toArray(children).find(
child => child.type === Layout.Footer
);
return (
<div className="layout">
<header className="layout-header">{header}</header>
<main className="layout-content">{content}</main>
<footer className="layout-footer">{footer}</footer>
</div>
);
}
// Define slot components
Layout.Header = function({ children }) { return children };
Layout.Content = function({ children }) { return children };
Layout.Footer = function({ children }) { return children };
// Usage
<Layout>
<Layout.Header>
<h1>Page Title</h1>
</Layout.Header>
<Layout.Content>
<p>Main page content</p>
</Layout.Content>
<Layout.Footer>
<p>Copyright 2023</p>
</Layout.Footer>
</Layout>
Advanced Patterns
1. Scoped Slots (Render Props Pattern)
function DataGrid({ data, columns, rowSlot }) {
return (
<table>
<thead>
<tr>
{columns.map(col => (
<th key={col.key}>{col.title}</th>
))}
</tr>
</thead>
<tbody>
{data.map((row, index) => (
<tr key={index}>
{columns.map(col => (
<td key={col.key}>
{rowSlot ? rowSlot({ row, column: col }) : row[col.key]}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}
// Usage
<DataGrid
data={users}
columns={[
{ key: 'name', title: 'Name' },
{ key: 'email', title: 'Email' }
]}
rowSlot={({ row, column }) => (
column.key === 'name'
? <strong>{row.name}</strong>
: row[column.key]
)}
/>
2. Compound Components with Slots
const Tabs = ({ children, defaultActive }) => {
const [activeTab, setActiveTab] = useState(defaultActive);
return Children.map(children, child => {
if (child.type === Tabs.Panel) {
return React.cloneElement(child, {
isActive: child.props.name === activeTab,
onSelect: () => setActiveTab(child.props.name)
});
}
return child;
});
};
Tabs.Panel = function({ name, isActive, onSelect, children }) {
return (
<div className={`tab-panel ${isActive ? 'active' : ''}`}>
<button onClick={onSelect}>{name}</button>
{isActive && <div className="tab-content">{children}</div>}
</div>
);
};
// Usage
<Tabs defaultActive="profile">
<Tabs.Panel name="dashboard">
Dashboard Content
</Tabs.Panel>
<Tabs.Panel name="profile">
Profile Content
</Tabs.Panel>
</Tabs>
3. Context-Based Slot Distribution
const SlotContext = createContext();
function SlotProvider({ slots, children }) {
return (
<SlotContext.Provider value={slots}>
{children}
</SlotContext.Provider>
);
}
function Slot({ name, children }) {
const slots = useContext(SlotContext);
return slots?.[name] || children || null;
}
// Usage in component library
function ComplexComponent() {
return (
<div className="complex-component">
<header>
<Slot name="header">
<h2>Default Header</h2>
</Slot>
</header>
<main>
<Slot name="content" />
</main>
</div>
);
}
// Usage by consumer
function App() {
return (
<SlotProvider slots={{
header: <h1>Custom Header</h1>,
content: <p>Custom content</p>
}}>
<ComplexComponent />
</SlotProvider>
);
}
Best Practices
- Clear Slot Naming: Use descriptive names for slots (e.g., “header”, “actions”, “footer”)
- Default Content: Provide sensible defaults for optional slots
- Type Safety: Use PropTypes or TypeScript to document slot expectations
- Performance: Avoid unnecessary re-renders with memoization
- Documentation: Clearly document available slots and their purposes
TypeScript Implementation
interface SlotProps {
name: string;
children?: React.ReactNode;
}
interface SlotProviderProps {
slots: Record<string, React.ReactNode>;
children: React.ReactNode;
}
const SlotContext = createContext<Record<string, React.ReactNode>>({});
const SlotProvider: React.FC<SlotProviderProps> = ({ slots, children }) => (
<SlotContext.Provider value={slots}>
{children}
</SlotContext.Provider>
);
const Slot: React.FC<SlotProps> = ({ name, children }) => {
const slots = useContext(SlotContext);
return slots?.[name] || children || null;
};
// Usage with typed slots
type AppSlots = {
header: React.ReactNode;
content: React.ReactNode;
};
function App() {
const slots: AppSlots = {
header: <h1>Typed Header</h1>,
content: <p>Typed Content</p>
};
return (
<SlotProvider slots={slots}>
<ComplexComponent />
</SlotProvider>
);
}
Real-World Examples
1. Modal Component with Slots
function Modal({ isOpen, onClose, children }) {
if (!isOpen) return null;
const title = Children.toArray(children).find(
child => child.type === Modal.Title
);
const body = Children.toArray(children).find(
child => child.type === Modal.Body
);
const footer = Children.toArray(children).find(
child => child.type === Modal.Footer
);
return (
<div className="modal-overlay">
<div className="modal">
<div className="modal-header">
{title}
<button onClick={onClose}>×</button>
</div>
<div className="modal-body">{body}</div>
{footer && <div className="modal-footer">{footer}</div>}
</div>
</div>
);
}
Modal.Title = function({ children }) { return <h2>{children}</h2> };
Modal.Body = function({ children }) { return <div>{children}</div> };
Modal.Footer = function({ children }) { return <div>{children}</div> };
// Usage
<Modal isOpen={showModal} onClose={() => setShowModal(false)}>
<Modal.Title>Confirm Action</Modal.Title>
<Modal.Body>
<p>Are you sure you want to proceed?</p>
</Modal.Body>
<Modal.Footer>
<button onClick={handleConfirm}>Confirm</button>
<button onClick={() => setShowModal(false)}>Cancel</button>
</Modal.Footer>
</Modal>
2. Data Table with Flexible Slots
function DataTable({ data, children }) {
const columns = Children.toArray(children).filter(
child => child.type === DataTable.Column
);
return (
<table>
<thead>
<tr>
{columns.map((column, index) => (
<th key={index}>{column.props.header}</th>
))}
</tr>
</thead>
<tbody>
{data.map((row, rowIndex) => (
<tr key={rowIndex}>
{columns.map((column, colIndex) => (
<td key={colIndex}>
{column.props.children
? column.props.children(row)
: row[column.props.field]}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}
DataTable.Column = function({ header, field, children }) {
return null; // Actual rendering happens in parent
};
// Usage
<DataTable data={products}>
<DataTable.Column header="ID" field="id" />
<DataTable.Column header="Name" field="name" />
<DataTable.Column header="Price">
{(product) => `$${product.price.toFixed(2)}`}
</DataTable.Column>
<DataTable.Column header="Actions">
{(product) => (
<button onClick={() => addToCart(product)}>
Add to Cart
</button>
)}
</DataTable.Column>
</DataTable>
Slot-based architecture provides a powerful way to create flexible component APIs that give consumers precise control over rendering while maintaining a clean internal structure. This pattern is particularly valuable for component libraries and complex UI systems.