Design flexible component APIs with compound components, the state reducer, and the right choice between render props and hooks.
Open this lesson in KodokonA <Tabs items={...} labels={...} icons={...} /> component always ends up buried under configuration props. The compound components pattern flips the approach: several components collaborate and share implicit state through a context, while the user keeps control of the JSX composition (order, nesting, interleaved elements). It is the model of <select> and <option> in HTML, and the one used by libraries like Radix or Headless UI.
const TabsContext = createContext(null);
function Tabs({ defaultTab, children }) {
const [active, setActive] = useState(defaultTab);
const value = useMemo(
() => ({ active, setActive }),
[active]
);
return (
<TabsContext.Provider value={value}>
{children}
</TabsContext.Provider>
);
}Each subcomponent reads the context and needs no wiring props at all: the user composes freely, and the pattern guarantees consistency. Note the context value wrapped in useMemo: without it, every render of the parent would create a new object and force a render of all consumers.
function Tab({ id, children }) {
const ctx = useContext(TabsContext);
return (
<button
aria-selected={ctx.active === id}
onClick={() => ctx.setActive(id)}
>
{children}
</button>
);
}
// <Tabs defaultTab="a">
// <Tab id="a">Profile</Tab>
// <Tab id="b">Settings</Tab>
// </Tabs>Second pattern: inversion of control, whose most refined form is the state reducer popularized by Kent C. Dodds in Downshift. Rather than adding a boolean prop for every new need, the hook exposes its state transitions: the caller provides a reducer that receives each action and can let it through, modify it, or cancel it. The library keeps its default behavior; fine-grained control belongs to the consumer.
function defaultReducer(state, action) {
if (action.type === 'toggle') {
return { on: !state.on };
}
return state;
}
function useToggle({ reducer = defaultReducer } = {}) {
const [state, dispatch] = useReducer(reducer, {
on: false,
});
const toggle = () => dispatch({ type: 'toggle' });
return { on: state.on, toggle };
}Finally, the eternal render props versus hooks debate. Hooks have won for sharing logic: no fake hierarchy in the tree, no pyramid of wrappers, natural composition of multiple sources. But the render prop keeps a legitimate territory: when the variation concerns the rendering itself. A virtualized list that asks you how to draw each row, a headless component that delegates all its JSX: there, a render function passed as a prop remains the most direct tool, because a hook cannot decide where to insert JSX in the caller's tree.
function MouseTracker({ render }) {
const [pos, setPos] = useState({ x: 0, y: 0 });
const onMove = (e) =>
setPos({ x: e.clientX, y: e.clientY });
return (
<div onMouseMove={onMove}>{render(pos)}</div>
);
}
// For the logic alone, a hook is enough:
// const pos = useMousePosition();