# React Composition Patterns

> Implements React composition patterns including compound components, state lifting, polymorphic components, slot-based layouts, and variant-driven designs for reusable component APIs.

- Skill: `paulpas/react-composition-patterns` (Agent Skill)
- Install (CLI): `npx skillmds@latest add paulpas/react-composition-patterns`
- Raw SKILL.md: https://api.skillmd.com/api/skills/paulpas/react-composition-patterns/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- License: MIT
- Author: paulpas (https://skillmd.com/u/paulpas)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/paulpas/react-composition-patterns

---


# React Composition Patterns

Transforms React component architecture by applying composition patterns — compound components, state lifting, polymorphic elements, and slot-based layouts — to create reusable, maintainable, and type-safe component APIs. Composition replaces inheritance as the primary mechanism for code reuse in React.

## TL;DR Checklist

- [ ] Extract reusable UI patterns into self-contained components before copy-pasting
- [ ] Use Compound Components with Context API for co-dependent pieces (Tabs, Accordion)
- [ ] Lift state to the nearest common ancestor when multiple children need shared data
- [ ] Pass data through props, never through refs or direct DOM queries
- [ ] Use `as` or `asChild` prop for polymorphic components with TypeScript generics
- [ ] Keep one component per file with colocated tests, stories, and styles
- [ ] Use Slot pattern for flexible, positional children instead of rigid prop APIs
- [ ] Drive visual variation through props (size, variant) not multiple separate components

---

## When to Use

Use this skill when:

- Building reusable component libraries or design systems
- Designing components with multiple visual variants (size, color, tone)
- Creating co-dependent UI patterns like Tabs, Accordion, Menu, or Stepper
- Needing a component to render as different HTML elements depending on context
- Passing shared state from a parent to multiple children without prop drilling
- Deciding between composition and inheritance for a new component API
- Reviewing component architecture for reusability and maintenance concerns

---

## When NOT to Use

Avoid this skill for:

- Simple, single-purpose components that don't compose with others (use a plain function component instead)
- Deeply nested prop drilling problems — use Context API or a state management library instead
- Components with only one variant or use case (over-engineering)
- Layout-only concerns better handled by CSS Grid or Flexbox (composition is for logic, not layout)
- Premature abstraction — don't create compound component APIs for components used once

---

## Core Workflow

1. **Identify Composable Patterns** — Look for repeated UI groupings in the design (label + input, tab panel, accordion item). **Checkpoint:** If you see the same 2-3 element combination in 3+ places, it's a composition candidate.

2. **Choose Composition Strategy** — Select the right pattern:
   - **Compound Components** → co-dependent pieces that share implicit state (Tabs, Accordion, Menu)
   - **Polymorphic** → component renders as different HTML elements via `as` prop
   - **Slot Pattern** → flexible layout with named/positional children
   - **Variant-Driven** → same component, different visual style via props

3. **Implement with TypeScript** — Write proper generics for polymorphic props, type-safe context for compound components. **Checkpoint:** Verify consumers get full type inference without manual type annotations.

4. **Share State via Context** — Use React Context (not prop threading) for compound component state. Place context provider in the parent compound wrapper.

5. **Export Pieces Individually** — Export every sub-component independently so consumers can import only what they use (enables tree shaking). **Checkpoint:** Verify each export is independently usable in tests.

6. **Test Composition Combinations** — Test each sub-component in isolation and all valid parent-child combinations. Edge cases: missing children, nested instances, dynamic children.

---

## Implementation Patterns

### Pattern 1: Compound Components with Context API

Compound components let related pieces share implicit state through Context, avoiding manual prop threading. The parent exposes a provider; children consume it.

```tsx
import React, { createContext, useContext, useState, useCallback } from 'react';

// Context holds shared state — never exported directly
interface TabsContextValue {
  activeTab: string;
  onSelect: (value: string) => void;
}

const TabsContext = createContext<TabsContextValue | null>(null);

function useTabsContext(): TabsContextValue {
  const ctx = useContext(TabsContext);
  if (!ctx) {
    throw new Error('Tab sub-components must be rendered within <Tabs>');
  }
  return ctx;
}

// ── Parent wrapper ──────────────────────────────────────
interface TabsProps {
  defaultTab: string;
  children: React.ReactNode;
}

function Tabs({ defaultTab, children }: TabsProps) {
  const [activeTab, setActiveTab] = useState(defaultTab);
  const onSelect = useCallback((value: string) => setActiveTab(value), []);

  return (
    <TabsContext.Provider value={{ activeTab, onSelect }}>
      {children}
    </TabsContext.Provider>
  );
}

// ── Sub-components ──────────────────────────────────────
function TabList({ children }: { children: React.ReactNode }) {
  return <div role="tablist" className="tab-list">{children}</div>;
}

interface TabProps {
  value: string;
  children: React.ReactNode;
}

function Tab({ value, children }: TabProps) {
  const { activeTab, onSelect } = useTabsContext();
  const isActive = activeTab === value;

  return (
    <button
      role="tab"
      aria-selected={isActive}
      onClick={() => onSelect(value)}
      className={isActive ? 'tab tab--active' : 'tab'}
    >
      {children}
    </button>
  );
}

interface TabPanelProps {
  value: string;
  children: React.ReactNode;
}

function TabPanel({ value, children }: TabPanelProps) {
  const { activeTab } = useTabsContext();
  if (activeTab !== value) return null;

  return (
    <div role="tabpanel" className="tab-panel">
      {children}
    </div>
  );
}

// Attach sub-components as properties for the fluent API
Tabs.TabList = TabList;
Tabs.Tab = Tab;
Tabs.TabPanel = TabPanel;

export { Tabs };

// Usage:
// <Tabs defaultTab="profile">
//   <Tabs.TabList>
//     <Tabs.Tab value="profile">Profile</Tabs.Tab>
//     <Tabs.Tab value="settings">Settings</Tabs.Tab>
//   </Tabs.TabList>
//   <Tabs.TabPanel value="profile">Profile content</Tabs.TabPanel>
//   <Tabs.TabPanel value="settings">Settings content</Tabs.TabPanel>
// </Tabs>
```

### Pattern 2: Polymorphic Component with TypeScript Generics

Polymorphic components accept an `as` prop to render as any HTML element while preserving type-safe props for that element.

```tsx
import React from 'react';

// ── Type-safe polymorphic props ─────────────────────────
type PolymorphicProps<
  T extends React.ElementType,
  P extends Record<string, unknown> = Record<string, unknown>
> = {
  as?: T;
  children: React.ReactNode;
} & P &
  Omit<React.ComponentPropsWithoutRef<T>, keyof (P & { as?: T; children: React.ReactNode })>;

// ── Component ───────────────────────────────────────────
interface TextProps {
  size?: 'sm' | 'md' | 'lg';
  weight?: 'normal' | 'bold';
}

function Text<T extends React.ElementType = 'span'>({
  as,
  size = 'md',
  weight = 'normal',
  children,
  ...rest
}: PolymorphicProps<T, TextProps>) {
  const Component = as || 'span';
  const className = `text text--${size} text--${weight}`;

  return <Component className={className} {...rest}>{children}</Component>;
}

export { Text };

// Usage:
// <Text as="h1" size="lg" weight="bold">Heading</Text>
// <Text as="p" size="sm">Paragraph with all native p props inferred</Text>
// <Text as="a" href="/page" size="md">Link styled as text</Text>
```

### Pattern 3: State Lifting (BAD vs. GOOD)

```tsx
// ❌ BAD: State trapped in child — sibling can't react to changes
function SearchInput() {
  const [query, setQuery] = useState('');
  // query is inaccessible to the parent or siblings
  return <input value={query} onChange={e => setQuery(e.target.value)} />;
}

function SearchResults() {
  // Can't access the query from SearchInput
  return <div>{/* no results to show */}</div>;
}

function SearchPage() {
  return (
    <div>
      <SearchInput />
      <SearchResults /> {/* Always empty — can't see the query */}
    </div>
  );
}

// ✅ GOOD: State lifted to common ancestor
function SearchInput({ query, onQueryChange }: {
  query: string;
  onQueryChange: (value: string) => void;
}) {
  return <input value={query} onChange={e => onQueryChange(e.target.value)} />;
}

function SearchResults({ query }: { query: string }) {
  // Can now access the query
  return <div>Results for: {query}</div>;
}

function SearchPage() {
  const [query, setQuery] = useState('');
  return (
    <div>
      <SearchInput query={query} onQueryChange={setQuery} />
      <SearchResults query={query} />
    </div>
  );
}
```

### Pattern 4: Slot-Based Layout

Slot patterns accept named children for flexible layout composition without tight coupling to specific child types.

```tsx
interface CardProps {
  /** Slot for the top-right action area */
  actions?: React.ReactNode;
  /** Slot for the main body content */
  body: React.ReactNode;
  /** Slot for the bottom footer area */
  footer?: React.ReactNode;
}

function Card({ actions, body, footer }: CardProps) {
  return (
    <div className="card">
      {actions && <div className="card__actions">{actions}</div>}
      <div className="card__body">{body}</div>
      {footer && <div className="card__footer">{footer}</div>}
    </div>
  );
}

// Usage:
// <Card
//   actions={<button onClick={handleClose}>✕</button>}
//   body={<p>Main content goes here</p>}
//   footer={<small>Last updated today</small>}
// />
```

---

## Constraints

### MUST DO
- Use Context API for compound component state — never thread props through intermediary components
- Provide TypeScript generics for polymorphic `as` prop so consumers get full type inference
- Export compound component sub-pieces individually for tree-shaking and independent testing
- Keep one component per file; colocate tests, stories, and styles beside the component
- Split components when they do more than one logical thing (Single Responsibility)
- Drive visual variants through size/variant props instead of creating separate components

### MUST NOT DO
- Pass data through refs or DOM queries — always use props for data flow
- Mutate children with `cloneElement` unless absolutely necessary (prefer render props or slots)
- Mix layout concerns with business logic in the same component
- Create compound component APIs for components used only once
- Use inheritance patterns for React component reuse

---

## Related Skills

| Skill | Purpose |
|---|---|
| `react-rerender-optimization` | Optimize re-renders once composition patterns are in place |
| `react-async-waterfalls` | Manage data dependencies in composed component trees |
| `react-server-performance` | Server Components and streaming for composed layouts |

---

## Live References

> Authoritative documentation links for this skill's domain. The model follows markdown links at load time to resolve external references and inline content.

- [React Composition vs Inheritance](https://react.dev/learn/composition-vs-inheritance)
- [React Compound Components Pattern](https://reactpatterns.com/#compound-component)
- [Polymorphic Components in TypeScript](https://www.typescriptlang.org/docs/handbook/jsx.html#function-component)
- [React Children Patterns (Slots)](https://react.dev/reference/react/Children)
- [Single Responsibility Principle in Components](https://en.wikipedia.org/wiki/Single-responsibility_principle)
- [React Context API](https://react.dev/reference/react/createContext)
- [Colocation principle (Kent C. Dodds)](https://kentcdodds.com/blog/colocation)

