Published on

React Composition Pattern

Authors

A Story of Building Better Components

Sarah was building a dashboard for her company. She started with a simple card component, but soon found herself drowning in prop hell.

"I need a card with a title and body," she thought. Easy enough:

<Card title="Welcome" body="Hello there!" />

But then the requirements changed. "The title needs an icon. Oh, and the body should have a button sometimes. And can the footer be different colors?"

Her component became a monster:

<Card
  title="Welcome"
  titleIcon="star"
  body="Hello there!"
  showButton={true}
  buttonText="Click me"
  footerColor="blue"
  footerText="Footer here"
  // ... 20 more props
/>

The Turning Point

A senior developer showed Sarah a different way: composition. Instead of passing everything as props, why not pass components as children?

<Card>
  <CardHeader>
    <Icon name="star" />
    <h2>Welcome</h2>
  </CardHeader>
  <CardBody>
    <p>Hello there!</p>
    <Button>Click me</Button>
  </CardBody>
  <CardFooter color="blue">Footer here</CardFooter>
</Card>

"This is just like HTML," Sarah realized. "You don't pass the contents of a <div> as props—you put things inside it!"

Non-Compositional vs. Compositional: A Side-by-Side Comparison

To truly understand the difference, let's examine the same component built both ways.

Example 1: The Modal Component

Non-Compositional Approach (Props-Heavy):

function Modal({
  isOpen,
  title,
  titleIcon,
  titleColor,
  message,
  messageAlign,
  showConfirmButton,
  confirmText,
  confirmColor,
  confirmIcon,
  onConfirm,
  showCancelButton,
  cancelText,
  cancelColor,
  onCancel,
  showFooter,
  footerText,
  footerAlign,
  allowClose,
  onClose,
  size,
  className,
  // ... 20 more props
}) {
  if (!isOpen) return null;

  return (
    <div className={`modal modal--${size} ${className}`}>
      <div className="modal-content">
        {allowClose && (
          <button className="modal-close" onClick={onClose}>×</button>
        )}

        {title && (
          <div className="modal-header" style={{ color: titleColor }}>
            {titleIcon && <Icon name={titleIcon} />}
            <h2>{title}</h2>
          </div>
        )}

        {message && (
          <div className="modal-body" style={{ textAlign: messageAlign }}>
            <p>{message}</p>
          </div>
        )}

        {(showConfirmButton || showCancelButton) && (
          <div className="modal-actions">
            {showCancelButton && (
              <button
                className={`btn btn--${cancelColor}`}
                onClick={onCancel}
              >
                {cancelText || 'Cancel'}
              </button>
            )}
            {showConfirmButton && (
              <button
                className={`btn btn--${confirmColor}`}
                onClick={onConfirm}
              >
                {confirmIcon && <Icon name={confirmIcon} />}
                {confirmText || 'Confirm'}
              </button>
            )}
          </div>
        )}

        {showFooter && footerText && (
          <div className="modal-footer" style={{ textAlign: footerAlign }}>
            {footerText}
          </div>
        )}
      </div>
    </div>
  );
}

// Usage - verbose and inflexible
<Modal
  isOpen={true}
  title="Delete Account"
  titleIcon="warning"
  titleColor="red"
  message="This action cannot be undone. Are you absolutely sure?"
  messageAlign="center"
  showConfirmButton={true}
  confirmText="Yes, delete my account"
  confirmColor="danger"
  onConfirm={handleDelete}
  showCancelButton={true}
  cancelText="No, keep it"
  onCancel={handleCancel}
  allowClose={true}
  onClose={handleCancel}
  size="medium"
/>

// What if you need a form inside? Add more props!
<Modal
  isOpen={true}
  title="Edit Profile"
  showForm={true}
  formFields={[...]}
  formValidation={...}
  onFormSubmit={...}
  // Props explosion continues...
/>

Compositional Approach:

// Simple, focused components
function Modal({ isOpen, onClose, size = 'medium', children }) {
  if (!isOpen) return null;

  return (
    <div className={`modal modal--${size}`}>
      <div className="modal-content">
        <button className="modal-close" onClick={onClose}>×</button>
        {children}
      </div>
    </div>
  );
}

function ModalHeader({ children, color }) {
  return (
    <div className="modal-header" style={{ color }}>
      {children}
    </div>
  );
}

function ModalBody({ children, align }) {
  return (
    <div className="modal-body" style={{ textAlign: align }}>
      {children}
    </div>
  );
}

function ModalActions({ children }) {
  return <div className="modal-actions">{children}</div>;
}

function ModalFooter({ children, align }) {
  return (
    <div className="modal-footer" style={{ textAlign: align }}>
      {children}
    </div>
  );
}

// Usage - clean and flexible
<Modal isOpen={true} onClose={handleCancel}>
  <ModalHeader color="red">
    <Icon name="warning" />
    <h2>Delete Account</h2>
  </ModalHeader>

  <ModalBody align="center">
    <p>This action cannot be undone.</p>
    <p>Are you absolutely sure?</p>
  </ModalBody>

  <ModalActions>
    <Button variant="secondary" onClick={handleCancel}>
      No, keep it
    </Button>
    <Button variant="danger" onClick={handleDelete}>
      <Icon name="trash" />
      Yes, delete my account
    </Button>
  </ModalActions>
</Modal>

// Need a form? Just put it inside! No new props needed
<Modal isOpen={true} onClose={handleClose}>
  <ModalHeader>
    <h2>Edit Profile</h2>
  </ModalHeader>

  <ModalBody>
    <form onSubmit={handleSubmit}>
      <Input label="Name" value={name} onChange={setName} />
      <Input label="Email" value={email} onChange={setEmail} />
      <TextArea label="Bio" value={bio} onChange={setBio} />

      <ModalActions>
        <Button type="button" onClick={handleClose}>Cancel</Button>
        <Button type="submit">Save Changes</Button>
      </ModalActions>
    </form>
  </ModalBody>
</Modal>

// Different layout? No problem!
<Modal isOpen={true} onClose={handleClose} size="large">
  <ModalBody>
    <div className="two-column-layout">
      <div className="column">
        <h3>Preview</h3>
        <ImagePreview src={image} />
      </div>
      <div className="column">
        <h3>Details</h3>
        <ImageMetadata data={metadata} />
      </div>
    </div>
  </ModalBody>

  <ModalFooter>
    <Button onClick={handleDownload}>Download</Button>
  </ModalFooter>
</Modal>

Example 2: The Alert/Notification Component

Non-Compositional Approach:

function Alert({
  type, // 'success' | 'error' | 'warning' | 'info'
  title,
  message,
  showIcon,
  icon,
  iconPosition,
  showCloseButton,
  onClose,
  showAction,
  actionText,
  actionVariant,
  onAction,
  showSecondaryAction,
  secondaryActionText,
  onSecondaryAction,
  dismissible,
  autoDismiss,
  dismissAfter,
  animateIn,
  animateOut,
  position,
  fullWidth,
  bordered,
  elevated,
  // ...more props
}) {
  const [isVisible, setIsVisible] = useState(true);

  useEffect(() => {
    if (autoDismiss) {
      setTimeout(() => setIsVisible(false), dismissAfter);
    }
  }, [autoDismiss, dismissAfter]);

  if (!isVisible) return null;

  const iconMap = {
    success: 'check-circle',
    error: 'x-circle',
    warning: 'alert-triangle',
    info: 'info-circle'
  };

  const displayIcon = icon || iconMap[type];

  return (
    <div
      className={`
        alert alert--${type}
        ${position ? `alert--${position}` : ''}
        ${fullWidth ? 'alert--full-width' : ''}
        ${bordered ? 'alert--bordered' : ''}
        ${elevated ? 'alert--elevated' : ''}
        ${animateIn ? 'alert--animate-in' : ''}
      `}
    >
      {showIcon && iconPosition === 'left' && (
        <Icon name={displayIcon} />
      )}

      <div className="alert-content">
        {title && <h4>{title}</h4>}
        {message && <p>{message}</p>}

        {(showAction || showSecondaryAction) && (
          <div className="alert-actions">
            {showSecondaryAction && (
              <button onClick={onSecondaryAction}>
                {secondaryActionText}
              </button>
            )}
            {showAction && (
              <button
                className={`btn--${actionVariant}`}
                onClick={onAction}
              >
                {actionText}
              </button>
            )}
          </div>
        )}
      </div>

      {showIcon && iconPosition === 'right' && (
        <Icon name={displayIcon} />
      )}

      {(dismissible || showCloseButton) && (
        <button className="alert-close" onClick={onClose}>×</button>
      )}
    </div>
  );
}

// Usage - trying to handle every scenario through props
<Alert
  type="error"
  title="Payment Failed"
  message="Your credit card was declined."
  showIcon={true}
  showAction={true}
  actionText="Update Payment Method"
  onAction={handleUpdatePayment}
  showCloseButton={true}
  onClose={handleClose}
  elevated={true}
/>

// What if you need custom content? Add more props!
<Alert
  type="info"
  customContent={<ComplexComponent />}  // Escape hatch
  // Now you have two systems: props AND custom content
/>

Compositional Approach:

// Focused, single-responsibility components
function Alert({ variant = 'info', children, onClose, dismissible }) {
  return (
    <div className={`alert alert--${variant}`}>
      {children}
      {dismissible && (
        <button className="alert-close" onClick={onClose}>×</button>
      )}
    </div>
  );
}

function AlertIcon({ name, variant }) {
  const iconMap = {
    success: 'check-circle',
    error: 'x-circle',
    warning: 'alert-triangle',
    info: 'info-circle'
  };

  return <Icon name={name || iconMap[variant]} />;
}

function AlertTitle({ children }) {
  return <h4 className="alert-title">{children}</h4>;
}

function AlertDescription({ children }) {
  return <div className="alert-description">{children}</div>;
}

function AlertActions({ children }) {
  return <div className="alert-actions">{children}</div>;
}

// Usage - clean and infinitely flexible
<Alert variant="error" dismissible onClose={handleClose}>
  <AlertIcon variant="error" />
  <div>
    <AlertTitle>Payment Failed</AlertTitle>
    <AlertDescription>
      Your credit card was declined.
    </AlertDescription>
    <AlertActions>
      <Button onClick={handleUpdatePayment}>
        Update Payment Method
      </Button>
    </AlertActions>
  </div>
</Alert>

// Complex custom content? No problem, no escape hatches needed!
<Alert variant="info">
  <AlertIcon variant="info" />
  <div>
    <AlertTitle>System Maintenance</AlertTitle>
    <AlertDescription>
      <p>We'll be performing maintenance on:</p>
      <ul>
        <li>Database servers: 2-3 AM EST</li>
        <li>API endpoints: 3-4 AM EST</li>
      </ul>
      <p>Expected downtime: <strong>2 hours</strong></p>
    </AlertDescription>
    <AlertActions>
      <a href="/status">View Status Page</a>
      <Button onClick={handleSubscribe}>
        Subscribe to Updates
      </Button>
    </AlertActions>
  </div>
</Alert>

// Different layout entirely? Just arrange differently!
<Alert variant="success">
  <div className="horizontal-alert">
    <AlertIcon variant="success" />
    <AlertDescription>
      Successfully saved! <a href="/view">View changes</a>
    </AlertDescription>
  </div>
</Alert>

The Key Differences: A Comparison Table

AspectNon-CompositionalCompositional
FlexibilityLimited to pre-defined propsInfinite combinations
API Surface20-50+ props per component2-5 props per component
Learning CurveMust learn all props upfrontLearn by composing familiar pieces
MaintainabilityChanges require modifying component internalsAdd new components, existing ones unchanged
TestingMust test every prop combinationTest small components independently
CustomizationNeed "escape hatch" props for edge casesEverything is already customizable
Bundle SizeLarge components with conditional logicSmall, tree-shakeable components
PredictabilityComplex internal logic, hidden behaviorWhat you see is what you get
ReusabilityComponents are specific, tightly coupledComponents are generic building blocks

Real Impact on Development

Scenario: Product Manager requests a change

"Can we add a video preview to the modal?"

Non-Compositional Response:

// Add new props to Modal component
function Modal({
  // ... 30 existing props
  showVideo,
  videoUrl,
  videoAutoplay,
  videoControls,
  videoThumbnail,
  // Now testing 35+ prop combinations...
}) {
  // Add conditional rendering logic
  // Hope it doesn't break existing usage
}

Compositional Response:

// No changes needed to any existing components!
<Modal isOpen={true}>
  <ModalHeader>
    <h2>Product Demo</h2>
  </ModalHeader>
  <ModalBody>
    <VideoPlayer src={videoUrl} autoplay controls thumbnail={thumbnail} />
  </ModalBody>
</Modal>

The compositional approach just works. No changes, no new props, no risk of breaking existing code.

Why Composition Wins

1. Flexibility Without Complexity

With composition, Sarah's Card component became simple:

function Card({ children }) {
  return <div className="card">{children}</div>
}

function CardHeader({ children }) {
  return <div className="card-header">{children}</div>
}

No conditional rendering. No prop explosion. Just simple containers.

2. Better Readability

Compare these two approaches:

Props approach:

<Modal
  isOpen={true}
  title="Delete Item"
  message="Are you sure?"
  confirmText="Yes, delete"
  cancelText="Cancel"
  onConfirm={handleDelete}
  onCancel={handleCancel}
/>

Composition approach:

<Modal isOpen={true}>
  <ModalTitle>Delete Item</ModalTitle>
  <ModalBody>Are you sure?</ModalBody>
  <ModalActions>
    <Button onClick={handleCancel}>Cancel</Button>
    <Button variant="danger" onClick={handleDelete}>
      Yes, delete
    </Button>
  </ModalActions>
</Modal>

The second version reads like a story. You can see the structure immediately.

3. Reusability

Sarah's individual components became reusable building blocks:

// Use CardHeader anywhere
<Dialog>
  <CardHeader>
    <Icon name="info" />
    <h2>Information</h2>
  </CardHeader>
</Dialog>

Real-World Scenario: Building a Product Card

Let's see composition in action. Sarah needs to build product cards for an e-commerce site.

The Requirements

  • Show product image, name, price, and rating
  • Some products are on sale (show badge)
  • Some products are out of stock (disable button)
  • Premium products get special styling

The Composition Solution

// Simple building blocks
function ProductCard({ children, isPremium }) {
  return <div className={`product-card ${isPremium ? 'premium' : ''}`}>{children}</div>
}

function ProductImage({ src, alt, badge }) {
  return (
    <div className="product-image">
      <img src={src} alt={alt} />
      {badge && <span className="badge">{badge}</span>}
    </div>
  )
}

function ProductInfo({ children }) {
  return <div className="product-info">{children}</div>
}

function ProductPrice({ original, sale }) {
  return (
    <div className="product-price">
      {sale ? (
        <>
          <span className="original">${original}</span>
          <span className="sale">${sale}</span>
        </>
      ) : (
        <span>${original}</span>
      )}
    </div>
  )
}

function ProductActions({ children }) {
  return <div className="product-actions">{children}</div>
}

Using the Components

Now Sarah can build any variation she needs:

// Regular product
<ProductCard>
  <ProductImage src="shoe.jpg" alt="Running Shoes" />
  <ProductInfo>
    <h3>Running Shoes</h3>
    <Rating value={4.5} />
    <ProductPrice original={89.99} />
  </ProductInfo>
  <ProductActions>
    <Button>Add to Cart</Button>
  </ProductActions>
</ProductCard>

// Product on sale
<ProductCard>
  <ProductImage
    src="jacket.jpg"
    alt="Winter Jacket"
    badge="SALE"
  />
  <ProductInfo>
    <h3>Winter Jacket</h3>
    <Rating value={5} />
    <ProductPrice original={199.99} sale={149.99} />
  </ProductInfo>
  <ProductActions>
    <Button>Add to Cart</Button>
  </ProductActions>
</ProductCard>

// Premium product, out of stock
<ProductCard isPremium>
  <ProductImage src="watch.jpg" alt="Luxury Watch" />
  <ProductInfo>
    <h3>Luxury Watch</h3>
    <Rating value={4.8} />
    <ProductPrice original={1299.99} />
    <p className="stock-status">Out of Stock</p>
  </ProductInfo>
  <ProductActions>
    <Button disabled>Notify Me</Button>
  </ProductActions>
</ProductCard>

The Key Insights

Through her journey, Sarah discovered why composition is powerful:

1. Think in Components, Not Props

  • Instead of "What props does this need?" ask "What parts does this have?"

2. Embrace the Children Prop

  • children is just a prop, but it's special—it lets you nest components naturally

3. Keep Components Focused

  • Each component does one thing well
  • Combine them to create complex UIs

4. Let Users Control Layout

  • Don't dictate how components are arranged
  • Provide building blocks and let users compose

The Transformation

Sarah's components became:

  • Easier to understand - each piece has a clear purpose
  • Easier to test - small components with minimal logic
  • Easier to change - modify one piece without breaking others
  • Easier to reuse - mix and match components anywhere

She had learned that the best React components aren't the ones with the most features—they're the ones that compose well with others.


Complex Real-World Scenarios

After mastering the basics, Sarah encountered more challenging situations where composition truly proved its worth.

Scenario 1: Multi-Step Form Wizard

Sarah needed to build a complex onboarding flow with validation, progress tracking, and conditional steps.

The Challenge: Different users see different steps based on their role. Some steps have sub-steps. Each step needs its own validation.

The Composition Solution:

// Core wizard components
function Wizard({ children, onComplete }) {
  const [currentStep, setCurrentStep] = useState(0)
  const [formData, setFormData] = useState({})

  const steps = React.Children.toArray(children)
  const CurrentStepComponent = steps[currentStep]

  const next = (data) => {
    setFormData((prev) => ({ ...prev, ...data }))
    if (currentStep < steps.length - 1) {
      setCurrentStep((prev) => prev + 1)
    } else {
      onComplete({ ...formData, ...data })
    }
  }

  const back = () => setCurrentStep((prev) => Math.max(0, prev - 1))

  return (
    <div className="wizard">
      <WizardProgress current={currentStep} total={steps.length} />
      {React.cloneElement(CurrentStepComponent, {
        onNext: next,
        onBack: back,
        data: formData,
        isFirst: currentStep === 0,
        isLast: currentStep === steps.length - 1,
      })}
    </div>
  )
}

function WizardStep({ title, children, onNext, onBack, isFirst, isLast, data }) {
  const [stepData, setStepData] = useState({})
  const [errors, setErrors] = useState({})

  const handleSubmit = (e) => {
    e.preventDefault()
    // Validate and proceed
    const validationErrors = children.props.validate?.(stepData) || {}
    if (Object.keys(validationErrors).length === 0) {
      onNext(stepData)
    } else {
      setErrors(validationErrors)
    }
  }

  return (
    <div className="wizard-step">
      <h2>{title}</h2>
      <form onSubmit={handleSubmit}>
        {React.cloneElement(children, {
          data: stepData,
          onChange: setStepData,
          errors,
        })}
        <div className="wizard-actions">
          {!isFirst && (
            <Button type="button" onClick={onBack}>
              Back
            </Button>
          )}
          <Button type="submit">{isLast ? 'Complete' : 'Next'}</Button>
        </div>
      </form>
    </div>
  )
}

function WizardProgress({ current, total }) {
  return (
    <div className="wizard-progress">
      {Array.from({ length: total }, (_, i) => (
        <div key={i} className={`step ${i <= current ? 'active' : ''}`} />
      ))}
    </div>
  )
}

Usage - Different flows for different users:

// For a customer
<Wizard onComplete={handleCustomerSignup}>
  <WizardStep title="Account Details">
    <AccountForm validate={validateAccount} />
  </WizardStep>
  <WizardStep title="Personal Info">
    <PersonalInfoForm validate={validatePersonal} />
  </WizardStep>
  <WizardStep title="Preferences">
    <PreferencesForm />
  </WizardStep>
</Wizard>

// For a business user - completely different steps
<Wizard onComplete={handleBusinessSignup}>
  <WizardStep title="Company Information">
    <CompanyForm validate={validateCompany} />
  </WizardStep>
  <WizardStep title="Team Members">
    <TeamMembersForm validate={validateTeam} />
  </WizardStep>
  <WizardStep title="Billing">
    <BillingForm validate={validateBilling} />
  </WizardStep>
  <WizardStep title="Integration">
    <IntegrationForm />
  </WizardStep>
</Wizard>

Scenario 2: Data Table with Complex Features

Sarah's team needed a table that supported sorting, filtering, pagination, row selection, expandable rows, and custom cell renderers.

The Challenge: Every feature combination creates exponential complexity. The table needs to work with any data type.

The Composition Solution:

// Flexible table system
function DataTable({ data, children }) {
  const [sortConfig, setSortConfig] = useState(null)
  const [selectedRows, setSelectedRows] = useState(new Set())
  const [expandedRows, setExpandedRows] = useState(new Set())

  const contextValue = {
    data,
    sortConfig,
    setSortConfig,
    selectedRows,
    setSelectedRows,
    expandedRows,
    setExpandedRows,
  }

  return (
    <TableContext.Provider value={contextValue}>
      <div className="data-table">{children}</div>
    </TableContext.Provider>
  )
}

function TableToolbar({ children }) {
  return <div className="table-toolbar">{children}</div>
}

function TableFilters({ children }) {
  return <div className="table-filters">{children}</div>
}

function TableHeader({ children }) {
  return (
    <thead>
      <tr>{children}</tr>
    </thead>
  )
}

function TableColumn({ field, sortable, children }) {
  const { sortConfig, setSortConfig } = useContext(TableContext)

  const handleSort = () => {
    if (!sortable) return
    setSortConfig({
      field,
      direction: sortConfig?.field === field && sortConfig.direction === 'asc' ? 'desc' : 'asc',
    })
  }

  return (
    <th onClick={handleSort} className={sortable ? 'sortable' : ''}>
      {children}
      {sortable && sortConfig?.field === field && (
        <span>{sortConfig.direction === 'asc' ? '↑' : '↓'}</span>
      )}
    </th>
  )
}

function TableBody({ children, renderRow }) {
  const { data, sortConfig, selectedRows, expandedRows } = useContext(TableContext)

  let processedData = [...data]

  if (sortConfig) {
    processedData.sort((a, b) => {
      const aVal = a[sortConfig.field]
      const bVal = b[sortConfig.field]
      const modifier = sortConfig.direction === 'asc' ? 1 : -1
      return aVal > bVal ? modifier : -modifier
    })
  }

  return (
    <tbody>
      {processedData.map((row, index) => (
        <React.Fragment key={row.id || index}>
          {renderRow(row, {
            isSelected: selectedRows.has(row.id),
            isExpanded: expandedRows.has(row.id),
          })}
        </React.Fragment>
      ))}
    </tbody>
  )
}

function TableRow({ id, children, expandable, expandedContent }) {
  const { selectedRows, setSelectedRows, expandedRows, setExpandedRows } = useContext(TableContext)

  const isSelected = selectedRows.has(id)
  const isExpanded = expandedRows.has(id)

  const toggleExpand = () => {
    setExpandedRows((prev) => {
      const next = new Set(prev)
      if (next.has(id)) next.delete(id)
      else next.add(id)
      return next
    })
  }

  return (
    <>
      <tr className={isSelected ? 'selected' : ''}>
        {expandable && (
          <td>
            <button onClick={toggleExpand}>{isExpanded ? '−' : '+'}</button>
          </td>
        )}
        {children}
      </tr>
      {expandable && isExpanded && (
        <tr className="expanded-row">
          <td colSpan="100%">{expandedContent}</td>
        </tr>
      )}
    </>
  )
}

Usage - Building a complex user management table:

function UserManagementTable() {
  const [users, setUsers] = useState([])
  const [filters, setFilters] = useState({ role: 'all', status: 'all' })

  const filteredUsers = users.filter((user) => {
    if (filters.role !== 'all' && user.role !== filters.role) return false
    if (filters.status !== 'all' && user.status !== filters.status) return false
    return true
  })

  return (
    <DataTable data={filteredUsers}>
      <TableToolbar>
        <h2>User Management</h2>
        <Button onClick={handleExport}>Export CSV</Button>
      </TableToolbar>

      <TableFilters>
        <Select value={filters.role} onChange={(role) => setFilters((f) => ({ ...f, role }))}>
          <option value="all">All Roles</option>
          <option value="admin">Admin</option>
          <option value="user">User</option>
        </Select>

        <Select value={filters.status} onChange={(status) => setFilters((f) => ({ ...f, status }))}>
          <option value="all">All Status</option>
          <option value="active">Active</option>
          <option value="inactive">Inactive</option>
        </Select>
      </TableFilters>

      <table>
        <TableHeader>
          <TableColumn field="name" sortable>
            Name
          </TableColumn>
          <TableColumn field="email" sortable>
            Email
          </TableColumn>
          <TableColumn field="role" sortable>
            Role
          </TableColumn>
          <TableColumn field="lastLogin" sortable>
            Last Login
          </TableColumn>
          <TableColumn>Actions</TableColumn>
        </TableHeader>

        <TableBody
          renderRow={(user) => (
            <TableRow id={user.id} expandable expandedContent={<UserDetailsPanel user={user} />}>
              <td>{user.name}</td>
              <td>{user.email}</td>
              <td>
                <Badge>{user.role}</Badge>
              </td>
              <td>{formatDate(user.lastLogin)}</td>
              <td>
                <IconButton onClick={() => handleEdit(user)}>✏️</IconButton>
                <IconButton onClick={() => handleDelete(user)}>🗑️</IconButton>
              </td>
            </TableRow>
          )}
        />
      </table>
    </DataTable>
  )
}

Scenario 3: Permission-Based UI System

Sarah's enterprise app needed complex permission controls where different users see different UI elements.

The Challenge: Permissions are hierarchical and contextual. Some features require multiple permissions. Need to avoid repeating permission logic everywhere.

The Composition Solution:

// Permission wrapper components
function PermissionGate({ requires, fallback = null, children }) {
  const { hasPermission } = usePermissions()

  // Handle single permission or array
  const permissions = Array.isArray(requires) ? requires : [requires]
  const hasAccess = permissions.every((p) => hasPermission(p))

  if (!hasAccess) return fallback
  return <>{children}</>
}

function AnyPermission({ requires, fallback = null, children }) {
  const { hasPermission } = usePermissions()

  const permissions = Array.isArray(requires) ? requires : [requires]
  const hasAccess = permissions.some((p) => hasPermission(p))

  if (!hasAccess) return fallback
  return <>{children}</>
}

function RoleGate({ roles, fallback = null, children }) {
  const { hasRole } = usePermissions()

  const roleList = Array.isArray(roles) ? roles : [roles]
  const hasAccess = roleList.some((r) => hasRole(r))

  if (!hasAccess) return fallback
  return <>{children}</>
}

// Composable feature flags
function FeatureFlag({ flag, fallback = null, children }) {
  const { isEnabled } = useFeatureFlags()

  if (!isEnabled(flag)) return fallback
  return <>{children}</>
}

// Smart component that combines permissions and features
function ProtectedFeature({ requires, feature, role, fallback = null, children }) {
  return (
    <PermissionGate requires={requires} fallback={fallback}>
      <FeatureFlag flag={feature} fallback={fallback}>
        <RoleGate roles={role} fallback={fallback}>
          {children}
        </RoleGate>
      </FeatureFlag>
    </PermissionGate>
  )
}

Usage - Building a dashboard with complex access control:

function Dashboard() {
  return (
    <div className="dashboard">
      <Header>
        <Logo />

        {/* Only admins and managers see analytics */}
        <AnyPermission requires={['view_analytics', 'manage_team']}>
          <NavLink to="/analytics">Analytics</NavLink>
        </AnyPermission>

        {/* Feature flag + permission check */}
        <ProtectedFeature
          requires="beta_access"
          feature="new_reports"
          fallback={<NavLink to="/reports">Reports (Classic)</NavLink>}
        >
          <NavLink to="/reports-v2">Reports (Beta)</NavLink>
        </ProtectedFeature>

        {/* Admin-only section */}
        <RoleGate roles="admin">
          <NavLink to="/admin">Admin Panel</NavLink>
        </RoleGate>
      </Header>

      <MainContent>
        {/* Different widgets based on permissions */}
        <PermissionGate requires="view_sales">
          <SalesWidget />
        </PermissionGate>

        <PermissionGate requires="view_team_performance" fallback={<RestrictedMessage />}>
          <TeamPerformanceWidget />
        </PermissionGate>

        {/* Requires ALL permissions */}
        <PermissionGate requires={['view_finances', 'view_forecasts']}>
          <FinancialForecastWidget />
        </PermissionGate>

        {/* Requires ANY permission */}
        <AnyPermission requires={['create_content', 'edit_content']}>
          <ContentManagementWidget />
        </AnyPermission>

        {/* Complex nested permissions */}
        <PermissionGate requires="view_reports">
          <ReportsSection>
            <PermissionGate requires="export_reports">
              <ExportButton />
            </PermissionGate>

            <PermissionGate requires="share_reports">
              <ShareButton />
            </PermissionGate>

            <FeatureFlag flag="advanced_filters">
              <AdvancedFilterPanel />
            </FeatureFlag>
          </ReportsSection>
        </PermissionGate>
      </MainContent>
    </div>
  )
}

The Lessons Learned

Through these complex scenarios, Sarah discovered deeper truths about composition:

1. Composition Scales Beautifully

  • Simple components combine to handle complex requirements
  • No need to predict every use case upfront

2. Context + Composition = Power

  • Use React Context to share state between composed components
  • Keeps the API clean while enabling deep integration

3. Flexibility Through Constraints

  • Each component does one thing well
  • Combining them creates infinite possibilities

4. Real-World Complexity Requires Real-World Patterns

  • Don't build generic "configure everything" components
  • Build focused components that compose into solutions

Sarah's codebase transformed from a tangled mess of props and conditionals into an elegant system of composable pieces. Her team could now build new features in hours instead of days, and maintenance became a joy instead of a nightmare.


"Make it work, then make it right, then make it fast. But most importantly, make it composable." - The path to maintainable React code.