Slot-Based Component Architecture

Loading

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:

  1. Named Slots: Designated areas for content injection
  2. Default Content: Fallback content when slots aren’t provided
  3. Scoped Slots: Slots that receive data from the child component
  4. 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

  1. Clear Slot Naming: Use descriptive names for slots (e.g., “header”, “actions”, “footer”)
  2. Default Content: Provide sensible defaults for optional slots
  3. Type Safety: Use PropTypes or TypeScript to document slot expectations
  4. Performance: Avoid unnecessary re-renders with memoization
  5. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *