Compound Components — implicit state via context

[check: …]
📖 guide (.md) ← react deep dive
📖 Pair this live React playground with the companion guide (.md) — this page is the rendered ground truth. ↗ The Context API is the foundation — compound components are the pattern built on top.

<select>/<option> in React: the compound pattern

In HTML, <select> and <option> cooperate with zero props between them — the select owns the state, each option reflects it. React gives you no built-in pairing, so you build one: a parent component holds the state and publishes it through a Context; related sub-components (attached as static properties, Tabs.Tab, Tabs.TabPanel) read it with useContext. The consumer writes <Tabs><Tabs.Tab/></Tabs> and never passes a state prop — exactly the clean ergonomics of <select>.

pieceroleanalogy
Context the shared channel, created once via createContext the invisible air between <select> and <option> that carries state
Parent (Tabs) holds state + wraps children in the Provider the <select> element — it alone owns open/selected
Static assignment (Tabs.Tab =) attaches sub-components as properties of the parent how the DOM "knows" <option> belongs to <select>
Child consumer (useContext) reads the shared state — no props passed in the <option> reflecting "selected" without being told

1 · the compound Tabs you write (edit me)

2 · Babel compiles JSX → element tree

<Tabs.Tab index={0}> becomes React.createElement(Tabs.Tab, {"{index:0}"}) — the member expression resolves to the statically-attached function. Each click calls ctx.setActive, the Provider republishes, and every consumer re-renders with the new value.

// (hit "compile & render" to see Babel's output)

3 · live React (implicit state, proven)

Click a tab heading — no props are passed between Tabs and Tabs.Tab; the active index flows through Context. The gold-check drives this automatically: assert tab-0 active, click tab-1 → tab-2 → tab-0, asserting the right panel mounts and the active flag moves each time.

active tab: · gold: tab-0 → click 1 → click 2 → click 0, panel + data-active match each step

intent → pattern

intentpatternwhy
share state implicitly var Ctx = createContext({}) + Provider in parent avoids prop-drilling through N intermediate levels
attach a sub-component Tabs.Tab = function (props) {"{...}"} static property → consumer writes Tabs.Tab, clean namespaced API
read the shared state var ctx = useContext(Ctx) inside a child no props passed; child stays decoupled from parent internals
conditionally mount a panel if (ctx.active !== index) return null only the active panel is in the tree — lazy-friendly
keep the context value stable useMemo(() ={"("} {"{active, setActive}"} {")"}, [active]) prevents every consumer re-rendering on unrelated parent renders