name: react-composition-patterns description: Implements React composition patterns including compound components, state lifting, polymorphic components, slot-based layouts, and variant-driven designs for reusable component APIs. license: MIT compatibility: opencode archetypes: - tactical - generation anti_triggers: - brainstorming - vague ideation - long-form architecture response_profile: verbosity: low directive_strength: high abstraction_level: operational metadata: version: "1.0.0" domain: coding triggers: react composition, compound components, react patterns, polymorphic components, slot pattern, component API, composition vs inheritance role: implementation scope: implementation output-format: code content-types: [code, guidance, do-dont, examples] related-skills: react-async-waterfalls, react-rerender-optimization, react-server-performance author: https://github.com/vercel source: https://github.com/vercel-labs/agent-skills
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
asorasChildprop 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
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.
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
asprop - Slot Pattern → flexible layout with named/positional children
- Variant-Driven → same component, different visual style via props
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.
Share State via Context — Use React Context (not prop threading) for compound component state. Place context provider in the parent compound wrapper.
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.
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.
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.
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)
// ❌ 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.
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
asprop 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
cloneElementunless 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.