References

React Composition Patterns

React-specific implementation of Composition Over Configuration patterns.


Core Patterns

  • Children Pattern (Composition Over Configuration)
  • Slots Pattern (Named Children)
  • Compound Components
  • Headless Components

Children Pattern (Composition Over Configuration)

// ✅ CORRECT: Flexible via children
function Card({ children }: { children: ReactNode }) {
  return <div className="rounded-lg border p-4 shadow-sm">{children}</div>;
}

// Consumer controls content
<Card>
  <h2>Title</h2>
  <p>Any content here</p>
  <Button>Action</Button>
</Card>

// ❌ WRONG: Configuration via props (rigid)
function Card({ title, description, buttonText }: CardProps) {
  return (
    <div className="rounded-lg border p-4 shadow-sm">
      <h2>{title}</h2>
      <p>{description}</p>
      <button>{buttonText}</button>
    </div>
  );
}

Rule: If content varies between uses, accept children instead of individual props.


Slots Pattern (Named Children)

Multiple content areas via named ReactNode props.

interface PageLayoutProps {
  header: ReactNode;
  sidebar?: ReactNode;
  children: ReactNode;
  footer?: ReactNode;
}

function PageLayout({ header, sidebar, children, footer }: PageLayoutProps) {
  return (
    <div className="flex flex-col min-h-screen">
      <header>{header}</header>
      <div className="flex flex-1">
        {sidebar && <aside className="w-64">{sidebar}</aside>}
        <main className="flex-1">{children}</main>
      </div>
      {footer && <footer>{footer}</footer>}
    </div>
  );
}

// Consumer decides slot content
<PageLayout
  header={<NavBar />}
  sidebar={<SideMenu items={menuItems} />}
  footer={<Footer />}
>
  <DashboardContent />
</PageLayout>

Compound Components

Share implicit state across related components via Context.

const TabsContext = createContext<{ active: string; setActive: (id: string) => void } | null>(null);

function Tabs({ defaultValue, children }: { defaultValue: string; children: ReactNode }) {
  const [active, setActive] = useState(defaultValue);
  return (
    <TabsContext.Provider value={{ active, setActive }}>
      <div role="tablist">{children}</div>
    </TabsContext.Provider>
  );
}

function TabTrigger({ value, children }: { value: string; children: ReactNode }) {
  const { active, setActive } = useContext(TabsContext)!;
  return (
    <button role="tab" aria-selected={active === value} onClick={() => setActive(value)}>
      {children}
    </button>
  );
}

function TabContent({ value, children }: { value: string; children: ReactNode }) {
  const { active } = useContext(TabsContext)!;
  if (active !== value) return null;
  return <div role="tabpanel">{children}</div>;
}

// Attach sub-components
Tabs.Trigger = TabTrigger;
Tabs.Content = TabContent;

// Usage
<Tabs defaultValue="tab1">
  <Tabs.Trigger value="tab1">Overview</Tabs.Trigger>
  <Tabs.Trigger value="tab2">Details</Tabs.Trigger>
  <Tabs.Content value="tab1"><Overview /></Tabs.Content>
  <Tabs.Content value="tab2"><Details /></Tabs.Content>
</Tabs>

When to use Context vs React.Children.map: Use Context for 4+ sub-components or deeply nested trees. Use React.Children.map for 2-3 immediate children only.


Headless Components

Behavior without styling — consumer controls all UI.

function useToggle(initial = false) {
  const [isOpen, setIsOpen] = useState(initial);
  return {
    isOpen,
    toggle: () => setIsOpen(prev => !prev),
    open:   () => setIsOpen(true),
    close:  () => setIsOpen(false),
    getToggleProps: () => ({
      onClick: () => setIsOpen(prev => !prev),
      'aria-expanded': isOpen,
    }),
    getContentProps: () => ({ hidden: !isOpen, role: 'region' }),
  };
}

// Consumer applies own styling
function FAQ({ question, answer }: { question: string; answer: string }) {
  const { getToggleProps, getContentProps } = useToggle();
  return (
    <div className="border-b">
      <button {...getToggleProps()} className="w-full text-left py-3 font-semibold">
        {question}
      </button>
      <div {...getContentProps()} className="pb-3 text-gray-600">
        {answer}
      </div>
    </div>
  );
}

Polymorphic Components

Render as different HTML elements via as prop.

type PolymorphicProps<E extends ElementType> = {
  as?: E;
  children: ReactNode;
} & Omit<ComponentPropsWithoutRef<E>, 'as' | 'children'>;

function Box<E extends ElementType = 'div'>({ as, children, ...props }: PolymorphicProps<E>) {
  const Component = as || 'div';
  return <Component {...props}>{children}</Component>;
}

// Same component, different elements
<Box>Default div</Box>
<Box as="section" className="mt-4">Section element</Box>
<Box as="a" href="/home">Link element</Box>

Prop-Heavy Anti-Pattern

// ❌ WRONG: 10+ props for content configuration
function Modal({
  title, subtitle, icon, body, footer,
  primaryAction, primaryLabel, secondaryAction, secondaryLabel,
}: ModalProps) { /* ... */ }

// ✅ CORRECT: Composition-based with sub-components
function Modal({ children, onClose }: { children: ReactNode; onClose: () => void }) {
  return <div className="modal">{children}</div>;
}
Modal.Header = ({ children }: { children: ReactNode }) => <div className="modal-header">{children}</div>;
Modal.Body   = ({ children }: { children: ReactNode }) => <div className="modal-body">{children}</div>;
Modal.Footer = ({ children }: { children: ReactNode }) => <div className="modal-footer">{children}</div>;

// Consumer composes freely
<Modal onClose={close}>
  <Modal.Header><h2>Confirm Delete</h2></Modal.Header>
  <Modal.Body><p>Are you sure?</p></Modal.Body>
  <Modal.Footer>
    <Button onClick={close}>Cancel</Button>
    <Button variant="danger" onClick={handleDelete}>Delete</Button>
  </Modal.Footer>
</Modal>

React Native

Same patterns apply — use children and named props for slots.

function ScreenLayout({ header, children, footer }: ScreenLayoutProps) {
  return (
    <SafeAreaView style={{ flex: 1 }}>
      {header && <View style={styles.header}>{header}</View>}
      <ScrollView style={styles.content}>{children}</ScrollView>
      {footer && <View style={styles.footer}>{footer}</View>}
    </SafeAreaView>
  );
}

<ScreenLayout header={<ScreenTitle title="Profile" />} footer={<TabBar />}>
  <ProfileContent user={user} />
</ScreenLayout>

TypeScript Tips

Type-safe generic data rendering:

// Render props with generics
interface ListProps<T> {
  items: T[];
  renderItem: (item: T, index: number) => ReactNode;
  keyExtractor: (item: T) => string;
}

function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return <ul>{items.map((item, i) => <li key={keyExtractor(item)}>{renderItem(item, i)}</li>)}</ul>;
}

// Usage
<List<User> items={users} keyExtractor={u => u.id} renderItem={u => <UserCard user={u} />} />

Cross-References