Command Palette

Search for a command to run...

Hectal
PHASE 10Intermediate ~31 min· topic 5 of 6

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.

Usage.jsxwhole filejsx
<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.

Context connects the partsdiagram
Rendering diagram…

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.

DataList.jsxwhole filejsx
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.

withToggle.jsxwhole filejsx
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 level

05The 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.

The same logic three ways, then as a hookdiagram
Rendering diagram…

Try it yourself

  1. 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. 2

    Add a third tab

    Add a Settings tab and panel. You add two lines and no state code: that is the payoff of compound components.

  3. 3

    Two HOCs, one prop name

    Write a second HOC withOpen that injects a prop named on from its own state, and wrap: withOpen(withToggle(FaqItem)). Which on does FaqItem get? (The inner withToggle renders FaqItem last, so its on wins and withOpen's is silently overwritten.) This name clash is one reason hooks won.

Code & diagrams

Compound components: accessible Tabs with context React 19+ New tab

`<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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

panel: Hi, I am Asha. I build React apps.
panel: No posts yet.
selected: false, true
Render props: renderItem and a function as children New tab

`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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Asha (admin) | Ravi (editor)
No invited users
Notifications on
A higher-order component, and the hook that replaces it New tab

Both FAQ items behave the same. In React DevTools the HOC adds a `withToggle(FaqItem)` layer above `FaqItem`; the hook version is one component.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

HOC component name: withToggle(FaqItem)
open answers: 2

Break 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>.

terminal
$ Console
── what you'll see ──
Uncaught Error: Tabs parts must be used inside <Tabs>
An error occurred in the <Tab> component.

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.

terminal
$ Preview
── what you'll see ──
The open answer closes every time the parent re-renders.

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 value and onValueChange props, and fall back to internal state when value is undefined (Topic 22.2). Radix's Tabs does exactly this.

  • ▸

    Attaching parts as static properties (Tabs.Tab) has two costs. A Server Component (Topic 20.2) that imports Tabs from a "use client" file gets a reference to client code, not the function itself, so it can't reach Tabs.Tab through 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, ref is a normal prop, so {...props} passes it; older code needed forwardRef) 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 memo child that receives it re-renders every time. Wrap it in useCallback if you measure a problem, or let the React Compiler do it (Topic 16.5).

Remember this

  1. 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 with useContext. 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. 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. 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. Using children as 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. 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 uses connect(...)(Component), and error-boundary helpers export withErrorBoundary.

  5. 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 injecting data), 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. 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: renderItem for lists and virtualised tables (Topic 24.1), renderOption in 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

01

What are compound components, and how do the parts share state?

02

What is a render prop? Give an example where it's still the best choice today.

03

What is a higher-order component, and what problems made hooks preferable?

04

For each pattern, name a situation where you would still choose it over a custom hook.

Practice

01

Build a compound Accordion with Accordion.Item, Accordion.Header and Accordion.Panel, where only one item is open at a time.

02

Write a withLogger(Component) HOC that logs rendering <Name> every time the wrapped component renders, and set its displayName.

03

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.