Topic 10.5
Reuse Patterns: Compound Components, Render Props and HOCs
In one line
Compound components share state through context so related parts can be arranged freely, render props let a component hand data to a function you supply, and higher-order components wrap a component to add props; hooks replaced most render props and HOCs, but each pattern still has a place.
Think of it like this
A set of building blocks for a dollhouse. The house frame (the parent) comes with walls, doors and windows (the parts) that you can arrange however you like, and they all know they belong to the same house. That's a compound component. A render prop is like a photo booth: it takes the photo (the data), and you decide what to print it on. A higher-order component is a gift-wrapping service: you hand it a present (a component), and you get it back wrapped with something extra.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Compound components
- A set of components (like
Tabs,Tab,Panel) that work together and share state through context. - Guard hook
- A small custom hook like
useTabs()that reads a context and throws a helpful error if no provider is found. - Render prop
- A prop whose value is a function returning JSX; the component calls it to let you decide what to draw.
- Function as children
- A render prop passed as
children:<Toggle>{(state) => ...}</Toggle>. - Higher-order component (HOC)
- A function that takes a component and returns a new component with extra props or behaviour:
withToggle(FaqItem). - Wrapper hell
- Many layers of wrapper components around one component, making the tree and the props hard to follow.
- displayName
- A property that sets a component's name in DevTools and warnings, often set by HOCs:
withToggle(FaqItem).
Step by step
01Compound components: the API you want
Start from how people should use it. The parts can be arranged freely, and nobody passes activeTab or onChange to each tab.
<Tabs defaultValue="about">
<Tabs.List label="Profile sections">
<Tabs.Tab value="about">About</Tabs.Tab>
<Tabs.Tab value="posts">Posts</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="about">Hi, I am Asha.</Tabs.Panel>
<Tabs.Panel value="posts">No posts yet.</Tabs.Panel>
</Tabs>02Context connects the parts
The parent keeps the state and provides it. Each part reads it through a guard hook. The tree between them can be anything.
03Render props: you supply the drawing
The component does the work and calls your function with the data. This is still the best tool when only the caller knows what each item should look like.
function DataList({ items, renderItem, empty = "Nothing here" }) {
if (items.length === 0) return <p>{empty}</p>;
return <ul>{items.map((item, i) => <li key={item.id}>{renderItem(item, i)}</li>)}</ul>;
}
<DataList items={users} renderItem={(u) => <b>{u.name}</b>} />04HOCs: wrap and inject props
A HOC returns a new component that renders the original with extra props. Set displayName so DevTools shows something useful, and pass all other props through.
function withToggle(Component) {
function WithToggle(props) {
const [on, setOn] = useState(false);
return <Component {...props} on={on} toggle={() => setOn((v) => !v)} />;
}
WithToggle.displayName = `withToggle(${Component.displayName || Component.name})`;
return WithToggle;
}
const FaqItemWithToggle = withToggle(FaqItem); // once, at module level05The same logic three ways, then as a hook
Render prop: <Toggle>{({ on, toggle }) => ...}</Toggle>. HOC: withToggle(FaqItem). Hook: const [on, toggle] = useToggle();. The hook adds no component to the tree and shows exactly where on comes from. That's why new code shares logic with hooks.
Try it yourself
- 1
Rearrange the tabs freely
In the Tabs example, put the panels above the tab list, or wrap one tab in a
<span title="New!">. Predict: does anything break? (No: the parts find their state through context, not through their position.) - 2
Add a third tab
Add a
Settingstab and panel. You add two lines and no state code: that is the payoff of compound components. - 3
Two HOCs, one prop name
Write a second HOC
withOpenthat injects a prop namedonfrom its own state, and wrap:withOpen(withToggle(FaqItem)). WhichondoesFaqItemget? (The innerwithTogglerendersFaqItemlast, so itsonwins andwithOpen's is silently overwritten.) This name clash is one reason hooks won.
Code & diagrams
`<TabsContext value>` is the React 19 provider syntax (Topic 9.2). Keyboard support with arrow keys, which real tabs need, is built in Topic 18.4.
Preview
Console
Nothing logged yet.Expected console output
panel: Hi, I am Asha. I build React apps.
panel: No posts yet.
selected: false, true`renderItem` is still everywhere in modern code. The `Toggle` version works too, but `const [on, toggle] = useToggle()` does the same with no extra component and no nesting.
Preview
Console
Nothing logged yet.Expected console output
Asha (admin) | Ravi (editor)
No invited users
Notifications onBoth FAQ items behave the same. In React DevTools the HOC adds a `withToggle(FaqItem)` layer above `FaqItem`; the hook version is one component.
Preview
Console
Nothing logged yet.Expected console output
HOC component name: withToggle(FaqItem)
open answers: 2Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Use a compound part outside its parent
Render <Tabs.Tab value="about">About</Tabs.Tab> on its own, outside any <Tabs>.
Break #2
Create a HOC inside a component
Move const FaqItemWithToggle = withToggle(FaqItem); inside a parent component that re-renders (for example, one with a counter button), then open the FAQ and click the counter.
Myth vs fact
Myth
Render props and HOCs are deprecated.
Fact
They're not deprecated and still work. Hooks replaced them for sharing stateful logic, but render props (renderItem) and HOCs (memo, withErrorBoundary) remain the right tool in specific cases.
Myth
Compound components need React.Children and cloneElement.
Fact
That was an older way to pass state to children, and it breaks as soon as a part is wrapped in another element. Context works at any depth and is the modern approach.
Myth
Hooks make components impossible to customise from outside.
Fact
Hooks share logic; render props and compound components share rendering control. Good libraries combine them: a useCombobox hook for logic plus compound parts or render props for the UI (headless components, Topic 22.3).
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
A compound parent can support controlled and uncontrolled use: accept
valueandonValueChangeprops, and fall back to internal state whenvalueis undefined (Topic 22.2). Radix'sTabsdoes exactly this. - ▸
Attaching parts as static properties (
Tabs.Tab) has two costs. A Server Component (Topic 20.2) that importsTabsfrom a"use client"file gets a reference to client code, not the function itself, so it can't reachTabs.Tabthrough a dot; it must import each part by name. And bundlers can't drop unused parts. That's why libraries such as Radix export named parts (TabsList,TabsTrigger,TabsContent). - ▸
HOCs must forward refs (in React 19,
refis a normal prop, so{...props}passes it; older code neededforwardRef) and should hoist non-React statics of the inner component. Forgetting either is a classic HOC bug. - ▸
A render prop function is a new function each render, so a
memochild that receives it re-renders every time. Wrap it inuseCallbackif you measure a problem, or let the React Compiler do it (Topic 16.5).
Remember this
- 1
Compound components are a group of components that work together and share hidden state, like HTML's
<select>and<option>. A<Tabs>parent holds the active tab in state and provides it through context (Topic 9.2);<Tabs.Tab>and<Tabs.Panel>read it withuseContext. The user of the component arranges the parts in any order, adds their own markup between them, and never wires state by hand. Many design systems (Radix, Reach, Headless UI, React Aria) are built this way. - 2
A compound component needs three things: a context created once, a parent that owns state and provides
{ value, setValue }, and parts that read it through a small guard hook (useTabs()) that throws a clear error if a part is used outside its parent. Attaching parts as properties (Tabs.Tab = Tab) is a common style that keeps them together in autocomplete; separate named exports (Tab,TabPanel) work just as well and suit tree-shaking and Server Components better. - 3
A render prop is a prop whose value is a function that returns JSX:
<DataList items={users} renderItem={(user) => <b>{user.name}</b>} />. The component owns the logic (looping, state, measurement) and calls your function to decide what to draw. Usingchildrenas that function is the same idea:<Toggle>{({ on, toggle }) => <button onClick={toggle}>{on ? "On" : "Off"}</button>}</Toggle>. Before 2019 this was the main way to share stateful logic (React Router v4, Formik, Downshift used it). - 4
A higher-order component (HOC) is a function that takes a component and returns a new component:
const FaqItemWithToggle = withToggle(FaqItem). The wrapper holds state or reads context and passes extra props to the inner component. It's the component version of a higher-order function (see /javascript on functions that take functions). You still meet HOCs:memo(Component)(Topic 16.3) is a HOC, older Redux code usesconnect(...)(Component), and error-boundary helpers exportwithErrorBoundary. - 5
Why hooks replaced most of them. Render props and HOCs both share logic by adding components to the tree. That causes wrapper hell (DevTools shows
withRouter(withTheme(connect(withToggle(FaqItem))))), prop name clashes (two HOCs both injectingdata), hidden inputs (you can't see which HOC supplied a prop), and deeply nested render-prop callbacks. A custom hook is a function call inside the component: inputs and outputs are visible, several hooks never clash because you name what they return, and nothing is added to the tree. - 6
When each still fits. Use compound components for UI families with shared state and flexible layout: tabs, accordions, menus, selects, steppers. Use a render prop when the parent must decide how to render something the child controls:
renderItemfor lists and virtualised tables (Topic 24.1),renderOptionin a combobox. Use a HOC when you must wrap many components the same way without touching their code (adding an error boundary,memo, or wiring to a legacy library), or in class component code bases (Topic 26.4) where hooks aren't available. For sharing stateful logic in function components, reach for a custom hook first.
Explain it without notes
What are compound components, and how do the parts share state?
What is a render prop? Give an example where it's still the best choice today.
What is a higher-order component, and what problems made hooks preferable?
For each pattern, name a situation where you would still choose it over a custom hook.
Practice
Build a compound Accordion with Accordion.Item, Accordion.Header and Accordion.Panel, where only one item is open at a time.
Write a withLogger(Component) HOC that logs rendering <Name> every time the wrapped component renders, and set its displayName.
Rewrite this render-prop component as a hook: <WindowWidth>{(w) => <p>{w}px</p>}</WindowWidth>.
Trade-offs
- ↔
Compound components give users layout freedom but a larger API to document, and context makes every part re-render when the shared value changes. For two or three fixed parts, a single component with props is simpler.
- ↔
Render props keep rendering in the caller's hands but nest quickly and create new functions each render. HOCs need no changes to the wrapped component but hide where props come from. Hooks are the default for logic; the other two are for rendering control and wrapping.
Done when you can
I can build compound components with a context, a parent and a guard hook.
I can write and use a render prop like
renderItem.I can write a HOC that passes props through and sets
displayName.I can explain wrapper hell and prop clashes, and why hooks avoid them.
I can pick the right pattern for a given reuse problem.