Component Design Patterns
The patterns you choose for component APIs determine whether your codebase scales gracefully or collapses under its own weight. Senior engineers don't just know these patterns â they know when each one is the right choice.
Compound Components
Compound components share implicit state through a parent, giving consumers flexible composition:
const Select = ({ children, onChange }: SelectProps) => {
const [value, setValue] = useState<string | null>(null);
const handleSelect = (v: string) => {
setValue(v);
onChange?.(v);
};
return (
<SelectContext.Provider value={{ value, onSelect: handleSelect }}>
<div role="listbox">{children}</div>
</SelectContext.Provider>
);
};
const Option = ({ value, children }: OptionProps) => {
const ctx = useContext(SelectContext);
const selected = ctx.value === value;
return (
<div
role="option"
aria-selected={selected}
onClick={() => ctx.onSelect(value)}
>
{children}
</div>
);
};
Select.Option = Option;
// Usage â consumers control layout and composition
<Select onChange={handleChange}>
<Select.Option value="a">Alpha</Select.Option>
<Select.Option value="b">Beta</Select.Option>
</Select>When to use: Component libraries, form controls, navigation â anywhere users need to compose children flexibly while sharing state.
Render Props
A component that takes a function as a child (or prop) and delegates rendering to the consumer:
function MouseTracker({ render }: { render: (pos: { x: number; y: number }) => ReactNode }) {
const [pos, setPos] = useState({ x: 0, y: 0 });
useEffect(() => {
const handler = (e: MouseEvent) => setPos({ x: e.clientX, y: e.clientY });
window.addEventListener('mousemove', handler);
return () => window.removeEventListener('mousemove', handler);
}, []);
return <>{render(pos)}</>;
}
// Usage
<MouseTracker render={({ x, y }) => <Tooltip x={x} y={y} />} />Render props were the go-to pattern before hooks. They're still valuable when you need inversion of control â letting the consumer decide what to render with the data.
Modern Alternative: Custom Hooks
Most render prop use cases are now better served by hooks:
function useMousePosition() {
const [pos, setPos] = useState({ x: 0, y: 0 });
useEffect(() => {
const handler = (e: MouseEvent) => setPos({ x: e.clientX, y: e.clientY });
window.addEventListener('mousemove', handler);
return () => window.removeEventListener('mousemove', handler);
}, []);
return pos;
}Use render props when you need to share behavior that includes JSX structure, not just data.
Higher-Order Components (HOCs)
A function that takes a component and returns an enhanced component:
function withAuth<P extends object>(Component: ComponentType<P>) {
return function AuthenticatedComponent(props: P) {
const { user, loading } = useAuth();
if (loading) return <Spinner />;
if (!user) return <Redirect to="/login" />;
return <Component {...props} />;
};
}
const ProtectedDashboard = withAuth(Dashboard);HOC pitfalls:
- Props get swallowed or collide (the "wrapper hell" problem)
- Debugging is harder â component names in DevTools are opaque
- Static methods and refs don't forward automatically
When HOCs still make sense: Cross-cutting concerns applied to many components (auth guards, error boundaries, analytics wrappers), especially when the concern includes conditional rendering.
Composition Over Inheritance
React explicitly favors composition. The children prop is the simplest composition tool:
function Card({ children }: { children: ReactNode }) {
return <div className="card">{children}</div>;
}
function CardWithHeader({ title, children }: { title: string; children: ReactNode }) {
return (
<Card>
<h2>{title}</h2>
<div>{children}</div>
</Card>
);
}Slot Pattern
For more complex layouts, use named slots via props:
function Layout({ sidebar, header, children }: LayoutProps) {
return (
<div className="layout">
<header>{header}</header>
<aside>{sidebar}</aside>
<main>{children}</main>
</div>
);
}
<Layout
header={<Nav />}
sidebar={<Menu />}
>
<Content />
</Layout>Pattern Selection Guide
| Pattern | Use when | Avoid when |
|---|---|---|
| Compound components | Flexible composition with shared state | Simple, non-composable UI |
| Render props | Consumer controls rendering with shared logic | A hook would suffice |
| HOCs | Cross-cutting concerns across many components | Only 1-2 components need it |
| Composition (children/slots) | Layout and structural patterns | State sharing is needed |
Interview Signal
Senior engineers demonstrate pattern literacy by explaining trade-offs, not just implementations. The answer to "which pattern?" is always "it depends" â followed by concrete criteria for the decision.