React Component Composition
Assemble reusable and adaptable user interfaces using compound components, slot patterns, and inversion of control.
TL;DR
- Assemble flexible component architectures using the
childrenprop. - Pass distinct layout sections using specialized
slotprops. - Share implicit state cleanly with declarative
compoundcomponents.
Slot Patterns
Named Slot PropsPass 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 FallbacksUse 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 EliminationPass elements directly to the layout.
// Invert control by composing directly
<Page header={<UserMenu user={currentUser} />} />Compound Components Pattern
Compound Parent with ContextSupply 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 TriggerRender 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 AttachmentAssign children directly onto parent namespace.
Tabs.Item = TabItem;
Tabs.Panel = TabPanel;Children Utilities and Polymorphism
Count Valid ChildrenCount child nodes ignoring null and false.
const count = React.Children.count(children);
return <Badge text={`${count} items`} />;Map Children ElementsIterate over children safely with Children.map.
return React.Children.map(children, (child) => (
<CardItem>{child}</CardItem>
));Polymorphic ComponentRender custom HTML tag dynamically using as prop.
export function Text({
as: Comp = 'span',
children,
}: TextProps) {
return <Comp>{children}</Comp>;
}Composition Best Practices
Avoid Prop ExplosionReplace fifty boolean flags with composed nodes.
// BAD: Rigid flag hell
<Dialog showOk showCancel okText="Yes" />
// GOOD: Composed layout
<Dialog actions={<ConfirmButtons />} />Flexible LayoutsLet callers govern DOM structure and alignment.
<ListWrapper>
<HeaderView>Title</HeaderView>
{items.map(renderItem)}
</ListWrapper>Preserve Ref ForwardingPass ref down through composed wrappers.
export function Box({
ref,
children,
}: BoxProps) {
return <Card ref={ref}>{children}</Card>;
}Tips
- Invert control by allowing consumer components to provide their own markup with
childrenrather than cluttering components with booleans. - Bundle sub-components directly onto parent component namespaces like
Tabs.Panelto provide intuitive auto-complete during development.
Warnings
- Avoid building monolithic components with dozens of rigid conditional
propsthat make design systems brittle and hard to extend. - Do not resurrect legacy higher-order component wrappers when simple component
compositionor custom hooks solve logic sharing cleanly.
In Practice
Build an accessible, declarative compound tab strip sharing active state through internal React context.
- Create TabsContext holding active tab identifier and setter.
- Build Tabs parent provider managing component state.
- Create Tab trigger buttons consuming context implicitly.
- Compose declarative tab navigation without prop threading.
type CtxVal = { id: string; set: (v: string) => void };
const Ctx =
createContext<CtxVal>({ id: '1', set: () => {} });
export function Tabs({ children }: any) {
const [id, set] = useState('1');
return (
<Ctx.Provider value={{ id, set }}>
<CardView>{children}</CardView>
</Ctx.Provider>
);
}
export function Tab({ val, text }: any) {
const { id, set } = useContext(Ctx);
const onPick = () => set(val);
return (
<Button onClick={onPick} active={id === val}>
{text}
</Button>
);
}FAQ
Inversion of control means letting the caller decide what to render inside specific slots rather than hardcoding presentation choices inside the component, making UI elements substantially more versatile.
A compound parent wraps its children in a dedicated Context Provider, allowing nested compound children to read and manipulate shared state without exposing explicit props to consumers.
Use children for single continuous layout streams. Use named slot props like header, sidebar, or footer when components have distinct structural regions that must accept custom elements.