Compound components
A menu component can be written as one black box: pass it buttonText and an items array, and it renders everything itself. That version works until it doesn't. The caller can't change how an item renders, can't reorder the pieces, can't reuse the button elsewhere, and every prop has to travel through Menu even when Menu itself never uses it. Compound components take the opposite approach: several small components designed to work together, with the caller composing them like markup.
HTML has worked this way from the start. A <select> is nothing without its <option>s, a <ul> styles the <li>s inside it, a <form> tracks the inputs it wraps, and the table family is a whole ecosystem of elements that only make sense together. Compound components bring that parent-and-children contract to your own React components.
The prop drilling problem
The monolithic menu drills in miniature: Menu receives buttonText and items purely to hand them to MenuButton and MenuDropdown. It cares about neither. That relay is prop drilling, and lifting state up covers when a layer or two of it is normal and when it stretches far enough to need a different tool. Across many layers it turns components into couriers, and refactoring any middle layer means rethreading the chain.
Sometimes the right fix is nothing. Drilling one or two levels is cheaper than a premature abstraction, and hasty abstractions cost more than the repetition they remove. The patterns below earn their keep when the drilling is real.
Flattening the structure
The compound version turns Menu into a wrapper that renders its children, and promotes the inner pieces to the caller's level:
const sports = ['Tennis', 'Pickleball', 'Racquetball', 'Squash']
<Menu>
<MenuButton>Sports</MenuButton>
<MenuDropdown>
{sports.map(sport => (
<MenuItem key={sport}>{sport}</MenuItem>
))}
</MenuDropdown>
</Menu>Each piece stays small and does one job, and the bodies behind that markup are close to one-liners:
function Menu({ children }) {
return <div className="menu">{children}</div>
}
function MenuButton({ children }) {
return <Button>{children}</Button>
}
function MenuDropdown({ children }) {
return <div className="menu-dropdown">{children}</div>
}
function MenuItem({ children }) {
return <div className="menu-item">{children}</div>
}MenuButton reuses the Button from the composition chapter. The structure that used to be hidden inside Menu is now visible at the call site, and the data slides directly to the component that uses it: the array is mapped right where it's needed, with no relay through Menu. Want each item to be a link instead of text? Put an <a> inside MenuItem. The component's author never has to anticipate that request.
The trade is transparency for verbosity. The caller writes more JSX and gets fine-grained control over every layer; the black box is gone in both senses.
The pieces are separate components, so a caller needs an import for each one. Libraries usually smooth that over by attaching the subcomponents to the parent before exporting it, which is plain JavaScript: functions are objects, so they can carry properties.
Menu.Button = MenuButton
Menu.Dropdown = MenuDropdown
Menu.Item = MenuItem
export default MenuOne import brings the whole family, and the call site, <Menu.Button>, advertises that the pieces belong together. The dot syntax is naming and nothing more. Whatever makes the pieces work together has to come from somewhere else, which is the gap the next section opens.
The missing piece: implicit state
The flattened menu has a hole in it: the button doesn't open the dropdown anymore. The open/closed boolean and its toggle function live in Menu, and Menu renders only its wrapping div and {children}. There's no prop being passed to MenuButton, so there's nowhere to hang the toggle. The pieces need to share state without the caller wiring it through every tag, which the community calls implicit state: the compound parts coordinate behind the scenes while the caller composes them.
React ships an API that can fake this. Children.map loops over a component's direct children (props.children has no guaranteed shape: several children give you an array, a single child gives you the element itself, and none gives you undefined, so calling .map() on it works right up until it doesn't), and cloneElement copies an element while injecting extra props:
import { Children, cloneElement, useState } from 'react'
function Menu({ children }) {
const [open, setOpen] = useState(false)
const toggle = () => setOpen(prevOpen => !prevOpen)
return (
<div className="menu">
{Children.map(children, child =>
cloneElement(child, { open, toggle })
)}
</div>
)
}Now every direct child of Menu silently receives open and toggle, and the button works again. It also breaks the moment anyone breathes on it, which is the real lesson.
Where Children.map falls short
There are two shortcomings, and both are structural. First, it's fragile: the mapping only touches direct children, so if a caller wraps MenuDropdown in a styling <div>, the div receives toggle and open instead, React complains in the console that a function is not a valid value for a DOM attribute, and the menu stops working. A compound component that forbids wrapping its pieces has given up the flexibility that justified it.
Second, it's depth-limited: MenuItem is a grandchild, so it receives nothing. Getting state to it means repeating the Children.map-plus-cloneElement dance inside MenuDropdown too, one relay per layer, which is prop drilling wearing a costume.
What the pattern actually needs is a way to teleport state from Menu to any descendant, however deep and however wrapped. That is exactly what context does, and the context chapter picks up this same menu and finishes it: Menu provides { open, toggle } to everything it wraps, and MenuButton reads the toggle with useContext no matter what sits between them.
Version note
The React docs now file Children and cloneElement under legacy APIs and recommend alternatives, context among them, for new code. They still work and still appear in plenty of existing codebases, which is why they're worth recognizing on sight. The course lessons and most older code write them with the default import as React.Children and React.cloneElement.
select and its options in HTML. You see the whole structure right where you write it, and each piece gets its content directly. The catch is the pieces still need to share things like "is the menu open," and the context chapter shows the clean way they do it.
Next up: Context, the tool that lets those pieces share state however deep they sit.

