Render props and headless components
Every component so far has had a face: it renders something you can see. A headless component has no styled UI of its own. It exists purely to provide behavior, and it renders whatever children you hand it. That sounds abstract until you notice how many widgets are the same behavior wearing different clothes: a favorite star, a dropdown menu, a dark-mode switch, and a "show more" section are all a boolean and a toggle function. A headless Toggle component captures that logic once, and each widget becomes markup composed around it.
This chapter follows the course in building that Toggle as a compound component family coordinated through context, then hits the wall that leads to the chapter's second idea: render props, the pattern for exposing a component's internal state to whoever renders it.
A headless Toggle family
Toggle owns a boolean and provides it, along with a toggle function, through a scoped ToggleContext provider. The rest of the family consumes that context, and each piece hangs off Toggle as a property (Toggle.Button = ToggleButton), the dot syntax from compound components:
<Toggle>
<Toggle.Button>
<Star />
</Toggle.Button>
<Toggle.On>The toggle is on</Toggle.On>
<Toggle.Off>The toggle is off</Toggle.Off>
</Toggle>Toggle.Buttonrenders its children in a<button type="button">with anonClickthat flips the state. The children don't need their own click handlers; a click on anything inside bubbles up to the wrapper, which is event bubbling doing the work, by way of React's synthetic event system rather than a listener on the button itself. A clickable<div>would behave the same for mouse users while losing keyboard activation and the screen-reader announcement, as accessibility covers.Toggle.Onrenders its children when the state is true, and null otherwise.Toggle.Offdoes the reverse.
The star icons, the menu markup, the labels, all come from the caller. That's what makes the component reusable across widgets that look nothing alike: the course drops the same Toggle under both the star and the menu, and the menu's own pieces wrap Toggle's pieces internally so the person using <Menu> never sees the machinery.
Letting the outside listen
A real star button doesn't only repaint; it tells a server someone clicked it. So the headless component needs an event-listener prop of its own, the same shape as onClick on your own components from the children and composition chapter. Toggle accepts an onToggle function and runs it from an effect whenever the state changes:
function Toggle({ children, onToggle = () => {} }) {
const [on, setOn] = useState(false)
const toggle = () => setOn(prevOn => !prevOn)
const firstRender = useRef(true)
useEffect(() => {
if (firstRender.current) {
firstRender.current = false
} else {
onToggle()
}
}, [on])
// the provider and the children render below
}Two small pieces of craft are hiding in there. The ref guards against the effect's first run: effects fire after the initial render too, and announcing a "toggle" that nobody performed is a bug, so the ref tracks whether this is still the first render without triggering re-renders the way state would. And the = () => {} default is a noop function, so a caller who doesn't care about the event doesn't crash the component when it calls onToggle() anyway.
The dependency array holds on alone. Adding onToggle, which is what exhaustive-deps asks for, would re-run the effect every time the caller passes a fresh inline function, so this is one of the spots where the lint rule and the intent disagree. One wrinkle in the guard also shows up in development. StrictMode runs each effect twice on mount, which spends the ref on the first run and lets the callback fire on the second. Production builds do not do this, and the durable fix is to derive the behavior from the change itself, comparing on against the previous value, rather than from whether this is the first render.
Render props: handing the inside out
Now the pattern hits a wall. Style a box with a CSS transition on its background color, render the filled version inside Toggle.On and the unfilled one inside Toggle.Off, and the transition never plays. The reason: Toggle.On and Toggle.Off mount and unmount their children. React isn't changing a class on one element; it's removing an element and inserting a different one, and CSS can't transition an element that stopped existing. What the caller needs is the on state itself, so one persistent element can vary its own class name. The state is trapped inside Toggle.
The escape hatch is a familiar move from JavaScript: callbacks invert control. When you call addEventListener("click", callback), you supply the function, but the browser calls it and the browser decides what it receives (the event object). A component can offer the same deal. Pass it a function, and the component will call that function with its internal state and render whatever the function returns:
function ToggleDisplay({ children }) {
const { on } = useContext(ToggleContext)
return children(on)
}
Toggle.Display = ToggleDisplay<Toggle.Display>
{on => <div className={`box ${on ? 'filled' : ''}`} />}
</Toggle.Display>The children of Toggle.Display aren't elements this time; they're a function. Toggle.Display calls it, passes in on, and returns the JSX it gets back. Now the div is one element that survives every toggle, only its class changes, and the transition plays. This is the render props pattern: a prop whose value is a function that the component calls to know what to render. Some APIs used a literal prop named render, which is where the pattern's name comes from; passing the function as children is the form that stuck.
And when you need the actual true-or-false value on your side, you pass the component a function as its child; the component calls your function with the value, and whatever your function returns is what appears on the page.
Next up: Custom hooks in practice, where the same logic moves into a function and the component disappears.

