React Component Composition

Assemble reusable and adaptable user interfaces using compound components, slot patterns, and inversion of control.

TL;DR

  1. Assemble flexible component architectures using the children prop.
  2. Pass distinct layout sections using specialized slot props.
  3. Share implicit state cleanly with declarative compound components.

Slot Patterns

    Named Slot Props

    Pass layout regions as JSX props.

    interface ShellProps {
      header: React.ReactNode;
      sidebar: React.ReactNode;
      children: React.ReactNode;
    }
    
    export function Shell({ header, sidebar, children }: ShellProps) {
      return <LayoutView>
        <HeaderView>{header}</HeaderView>
        <RowView>
          <SidebarView>{sidebar}</SidebarView>
          <MainView>{children}</MainView>
        </RowView>
      </LayoutView>;
    }
    Default Slot Fallbacks

    Use a default when a slot is omitted.

    export function Card({
      title, action = <Button>Action</Button>,
    }: CardProps) {
      return <CardView>
        <CardTitle>{title}</CardTitle>
        {action}
      </CardView>;
    }
    Prop Drilling Elimination

    Pass elements directly to the layout.

    // Invert control by composing directly
    <Page header={<UserMenu user={currentUser} />} />

Compound Components Pattern

    Compound Parent with Context

    Supply shared active selection to children.

    const TabsCtx =
      createContext<TabsContext | null>(null);
    
    export function Tabs({
      active,
      onChange,
      children,
    }: TabsProps) {
      return (
        <TabsCtx.Provider value={{ active, onChange }}>
          <CardView>{children}</CardView>
        </TabsCtx.Provider>
      );
    }
    Compound Child Trigger

    Render tab trigger connected to shared context.

    export function TabItem({
      id,
      label,
    }: { id: string; label: string }) {
      const ctx = useContext(TabsCtx);
      const isSelected = ctx?.active === id;
      return (
        <Button
          selected={isSelected}
          onClick={() => ctx?.onChange(id)}
        >
          {label}
        </Button>
      );
    }
    Static Sub-Component Attachment

    Assign children directly onto parent namespace.

    Tabs.Item = TabItem;
    Tabs.Panel = TabPanel;

Children Utilities and Polymorphism

    Count Valid Children

    Count child nodes ignoring null and false.

    const count = React.Children.count(children);
    return <Badge text={`${count} items`} />;
    Map Children Elements

    Iterate over children safely with Children.map.

    return React.Children.map(children, (child) => (
      <CardItem>{child}</CardItem>
    ));
    Polymorphic Component

    Render custom HTML tag dynamically using as prop.

    export function Text({
      as: Comp = 'span',
      children,
    }: TextProps) {
      return <Comp>{children}</Comp>;
    }

Composition Best Practices

    Avoid Prop Explosion

    Replace fifty boolean flags with composed nodes.

    // BAD: Rigid flag hell
    <Dialog showOk showCancel okText="Yes" />
    
    // GOOD: Composed layout
    <Dialog actions={<ConfirmButtons />} />
    Flexible Layouts

    Let callers govern DOM structure and alignment.

    <ListWrapper>
      <HeaderView>Title</HeaderView>
      {items.map(renderItem)}
    </ListWrapper>
    Preserve Ref Forwarding

    Pass ref down through composed wrappers.

    export function Box({
      ref,
      children,
    }: BoxProps) {
      return <Card ref={ref}>{children}</Card>;
    }

Tips

  1. Invert control by allowing consumer components to provide their own markup with children rather than cluttering components with booleans.
  2. Bundle sub-components directly onto parent component namespaces like Tabs.Panel to provide intuitive auto-complete during development.

Warnings

  1. Avoid building monolithic components with dozens of rigid conditional props that make design systems brittle and hard to extend.
  2. Do not resurrect legacy higher-order component wrappers when simple component composition or custom hooks solve logic sharing cleanly.

In Practice

FAQ