Component Composition

Learn how to use compound components and composition patterns to build flexible, reusable UI components.

What is Component Composition?

Component composition is a pattern where complex components are built by combining simpler, focused components that work together. Matrix UI extensively uses compound components to provide flexibility while maintaining consistency.

Compound Components

Compound components are a group of components that work together to form a complete UI pattern. They share implicit state and communicate with each other internally.

Card Component Family

The Card component demonstrates the compound pattern perfectly:

import { 
  Card, 
  CardHeader, 
  CardTitle, 
  CardDescription, 
  CardContent, 
  CardFooter 
} from '@matrix-ui/components'

function UserProfile() {
  return (
    <Card>
      <CardHeader>
        <CardTitle>John Doe</CardTitle>
        <CardDescription>Senior Developer</CardDescription>
      </CardHeader>
      <CardContent>
        <p>Building amazing web applications with React and TypeScript.</p>
      </CardContent>
      <CardFooter>
        <Button>Contact</Button>
        <Button variant="outline">View Profile</Button>
      </CardFooter>
    </Card>
  )
}

Dialog Component Family

import { 
  Dialog, 
  DialogTrigger, 
  DialogContent, 
  DialogHeader, 
  DialogTitle, 
  DialogDescription,
  DialogFooter,
  DialogClose
} from '@matrix-ui/components'

function ConfirmDialog() {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button variant="destructive">Delete Account</Button>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Are you sure?</DialogTitle>
          <DialogDescription>
            This action cannot be undone. This will permanently delete 
            your account and remove your data from our servers.
          </DialogDescription>
        </DialogHeader>
        <DialogFooter>
          <DialogClose asChild>
            <Button variant="outline">Cancel</Button>
          </DialogClose>
          <Button variant="destructive">Delete Account</Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}

Alert Component Family

import { Alert, AlertTitle, AlertDescription } from '@matrix-ui/components'
import { AlertTriangle } from 'lucide-react'

function StatusAlert() {
  return (
    <Alert variant="destructive">
      <AlertTriangle className="h-4 w-4" />
      <AlertTitle>Error</AlertTitle>
      <AlertDescription>
        Your session has expired. Please log in again.
      </AlertDescription>
    </Alert>
  )
}

Composition Benefits

Flexibility

Components can be rearranged and customized

  • • Mix and match sub-components as needed
  • • Skip optional parts when not required
  • • Customize individual component props
  • • Override default behavior selectively

Maintainability

Easier to understand and modify

  • • Single responsibility principle
  • • Clear component boundaries
  • • Isolated concerns
  • • Easy to test individual parts

Reusability

Components work in different contexts

  • • Sub-components can be used independently
  • • Consistent API across similar patterns
  • • Standardized composition patterns
  • • Reduced code duplication

Composition Patterns

Slot Pattern

Use slots to define flexible content areas:

// Good: Using slots for flexible layout
<Card>
  <CardHeader>
    {/* Header slot - optional */}
    <CardTitle>User Settings</CardTitle>
  </CardHeader>
  <CardContent>
    {/* Content slot - main content */}
    <SettingsForm />
  </CardContent>
  <CardFooter>
    {/* Footer slot - actions */}
    <Button>Save</Button>
    <Button variant="outline">Cancel</Button>
  </CardFooter>
</Card>

// Bad: Monolithic component
<SettingsCard 
  title="User Settings"
  content={<SettingsForm />}
  primaryAction="Save"
  secondaryAction="Cancel"
/>

Provider Pattern

Some compound components use React context for internal communication:

// Dialog uses context internally
<Dialog> {/* Provider */}
  <DialogTrigger> {/* Consumer */}
    <Button>Open Dialog</Button>
  </DialogTrigger>
  <DialogContent> {/* Consumer */}
    <DialogTitle>Dialog Title</DialogTitle>
  </DialogContent>
</Dialog>

// Context shares state between components:
// - Open/close state
// - Focus management
// - Accessibility attributes

Render Props Pattern

Use render props for dynamic content:

// Custom composition using render props
function DataCard({ data, renderHeader, renderContent }) {
  return (
    <Card>
      {renderHeader && (
        <CardHeader>
          {renderHeader(data)}
        </CardHeader>
      )}
      <CardContent>
        {renderContent(data)}
      </CardContent>
    </Card>
  )
}

// Usage
<DataCard 
  data={user}
  renderHeader={(user) => (
    <>
      <CardTitle>{user.name}</CardTitle>
      <CardDescription>{user.role}</CardDescription>
    </>
  )}
  renderContent={(user) => (
    <UserProfile user={user} />
  )}
/>

Advanced Composition

Conditional Rendering

function ProductCard({ product, showActions = true, showDescription = true }) {
  return (
    <Card>
      <CardHeader>
        <CardTitle>{product.name}</CardTitle>
        {showDescription && product.description && (
          <CardDescription>{product.description}</CardDescription>
        )}
      </CardHeader>
      <CardContent>
        <div className="text-2xl font-bold">${product.price}</div>
      </CardContent>
      {showActions && (
        <CardFooter>
          <Button>Add to Cart</Button>
          <Button variant="outline">View Details</Button>
        </CardFooter>
      )}
    </Card>
  )
}

Dynamic Composition

function DynamicAlert({ type, title, description, actions }) {
  return (
    <Alert variant={type}>
      {title && <AlertTitle>{title}</AlertTitle>}
      {description && <AlertDescription>{description}</AlertDescription>}
      {actions && (
        <div className="flex gap-2 mt-2">
          {actions.map((action, index) => (
            <Button key={index} {...action.props}>
              {action.label}
            </Button>
          ))}
        </div>
      )}
    </Alert>
  )
}

// Usage
<DynamicAlert
  type="destructive"
  title="Authentication Error"
  description="Please check your credentials and try again."
  actions={[
    { label: "Retry", props: { onClick: handleRetry } },
    { label: "Cancel", props: { variant: "outline", onClick: handleCancel } }
  ]}
/>

Best Practices

1. Keep Components Focused

  • Each component should have a single responsibility
  • Avoid creating overly complex monolithic components
  • Prefer composition over complex prop APIs

2. Maintain Consistent APIs

  • Use consistent naming patterns across component families
  • Follow established prop conventions
  • Provide sensible defaults for optional components

3. Consider Accessibility

  • Ensure proper ARIA relationships between components
  • Handle keyboard navigation across compound components
  • Maintain focus management in interactive patterns

4. Performance Considerations

  • Use React.memo() for frequently rendered sub-components
  • Avoid unnecessary re-renders by optimizing context usage
  • Consider lazy loading for optional sub-components