| name | react-composition |
| license | MIT |
| description | React component composition patterns for building flexible, maintainable UIs. Covers compound components, context-based state, explicit variants, and React 19 APIs. Use when designing component APIs, refactoring prop-heavy components, or building reusable component libraries. |
React Composition
Build flexible component APIs through composition instead of configuration.
Core Principle
Composition over configuration. When a component needs a new behavior, the answer is almost never "add a boolean prop." Instead, compose smaller pieces together.
<Modal
hasHeader
hasFooter
hasCloseButton
isFullScreen
isDismissable
hasOverlay
centerContent
/>
<Modal>
<Modal.Header>
<Modal.Title>Settings</Modal.Title>
<Modal.Close />
</Modal.Header>
<Modal.Body>...</Modal.Body>
<Modal.Footer>
<Button onClick={save}>Save</Button>
</Modal.Footer>
</Modal>
Pattern 1: Compound Components
Share implicit state through context. Each sub-component is independently meaningful.
interface TabsContextValue {
activeTab: string;
setActiveTab: (tab: string) => void;
}
const TabsContext = createContext<TabsContextValue | null>(null);
function useTabs() {
const ctx = use(TabsContext);
if (!ctx) throw new Error('useTabs must be used within <Tabs>');
return ctx;
}
function Tabs({ defaultTab, children }: { defaultTab: string; children: React.ReactNode }) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabsContext value={{ activeTab, setActiveTab }}>
<div role="tablist">{children}</div>
</TabsContext>
);
}
function TabTrigger({ value, children }: { value: string; children: React.ReactNode }) {
const { activeTab, setActiveTab } = useTabs();
return (
<button
role="tab"
aria-selected={activeTab === value}
onClick={() => setActiveTab(value)}
>
{children}
</button>
);
}
function TabContent({ value, children }: { value: string; children: React.ReactNode }) {
const { activeTab } = useTabs();
if (activeTab !== value) return null;
return <div role="tabpanel">{children}</div>;
}
Tabs.Trigger = TabTrigger;
Tabs.Content = TabContent;
Pattern 2: Explicit Variants
When components have distinct modes, create explicit variant components instead of boolean switches.
<Input bordered />
<Input underlined />
<Input ghost />
<Input.Bordered placeholder="Name" />
<Input.Underlined placeholder="Name" />
<Input.Ghost placeholder="Name" />
function createInputVariant(className: string) {
return forwardRef<HTMLInputElement, InputProps>((props, ref) => (
<InputBase ref={ref} className={cn(className, props.className)} {...props} />
));
}
Input.Bordered = createInputVariant('border border-gray-300 rounded-md px-3 py-2');
Input.Underlined = createInputVariant('border-b border-gray-300 px-1 py-2');
Input.Ghost = createInputVariant('bg-transparent px-3 py-2');
Pattern 3: Children Over Render Props
Use children for composition. Only use render props when the child needs data from the parent.
<Card renderHeader={() => <h2>Title</h2>} renderBody={() => <p>Content</p>} />
<Card>
<Card.Header><h2>Title</h2></Card.Header>
<Card.Body><p>Content</p></Card.Body>
</Card>
<Combobox>
{({ isOpen, selectedItem }) => (
<>
<Combobox.Input />
{isOpen && <Combobox.Options />}
{selectedItem && <Badge>{selectedItem.label}</Badge>}
</>
)}
</Combobox>
Pattern 4: Context Interface Design
Design context interfaces with clear separation of state, actions, and metadata.
interface FormContext<T> {
values: T;
errors: Record<string, string>;
touched: Record<string, boolean>;
setValue: (field: keyof T, value: T[keyof T]) => void;
setTouched: (field: keyof T) => void;
validate: () => boolean;
submit: () => Promise<void>;
isSubmitting: boolean;
isDirty: boolean;
isValid: boolean;
}
State Lifting
Move state into provider when siblings need access.
function Parent() {
const [selected, setSelected] = useState<string | null>(null);
return (
<>
<Sidebar selected={selected} onSelect={setSelected} />
<Detail selected={selected} />
</>
);
}
function Parent() {
return (
<SelectionProvider>
<Sidebar />
<Detail />
</SelectionProvider>
);
}
React 19 APIs
Drop forwardRef
React 19 passes ref as a regular prop.
const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => (
<input ref={ref} {...props} />
));
function Input({ ref, ...props }: InputProps & { ref?: React.Ref<HTMLInputElement> }) {
return <input ref={ref} {...props} />;
}
use() Instead of useContext()
const ctx = useContext(ThemeContext);
const ctx = use(ThemeContext);
Decision Guide
| Situation | Pattern |
|---|
| Component has 3+ boolean layout props | Compound components |
| Multiple visual modes of same component | Explicit variants |
| Parent data needed in flexible child layout | Render prop |
| Siblings share state | Context provider + state lifting |
| Simple customization of a slot | children prop |
| Component needs imperative API | useImperativeHandle |
Anti-Patterns
| Avoid | Why | Instead |
|---|
<Component isX isY isZ /> | Combinatorial explosion, unclear interactions | Compound components or explicit variants |
renderHeader, renderFooter | Couples parent API to child structure | children + slot components |
| Deeply nested context providers | Performance + debugging nightmare | Colocate state with consumers, split contexts |
React.cloneElement for injection | Fragile, breaks with wrappers | Context-based composition |
| Single mega-context for all state | Every consumer re-renders on any change | Split into StateContext + ActionsContext |