Advanced Patterns

Course 3 · Ch 5
Advanced Patterns — Compound Components, Render Props, HOCs
Three classic approaches to flexible component APIs — one still common, two largely superseded by hooks

These three patterns predate (in two cases) or coexist with the hooks covered throughout this course, and component libraries you'll use professionally still rely on all three in places. Worth recognizing each one on sight, even where a custom hook (Intermediate Chapter 4) would now be the more natural choice for new code.

Compound Components — Sharing State Implicitly

const TabsContext = createContext(null); function Tabs({ children }) { const [activeTab, setActiveTab] = useState(0); return <TabsContext.Provider value={{ activeTab, setActiveTab }}>{children}</TabsContext.Provider>; } function Tab({ index, children }) { const { activeTab, setActiveTab } = useContext(TabsContext); return ( <button onClick={() => setActiveTab(index)} style={{ fontWeight: activeTab === index ? "bold" : "normal" }}> {children} </button> ); } function Panel({ index, children }) { const { activeTab } = useContext(TabsContext); return activeTab === index ? <div>{children}</div> : null; } Tabs.Tab = Tab; Tabs.Panel = Panel; // usage — looks like one cohesive widget, no explicit state passed by the caller <Tabs> <Tabs.Tab index={0}>Profile</Tabs.Tab> <Tabs.Tab index={1}>Settings</Tabs.Tab> <Tabs.Panel index={0}>Profile content</Tabs.Panel> <Tabs.Panel index={1}>Settings content</Tabs.Panel> </Tabs>

A compound component is a set of components designed to be used together, internally coordinating via Context the way Tabs does here — the consumer never sees activeTab or passes it anywhere explicitly; Tabs, Tabs.Tab, and Tabs.Panel all share it transparently through context set up entirely inside the family. Attaching Tab and Panel as properties on Tabs (Tabs.Tab = Tab) is purely a naming/organization convention, making clear which pieces belong together without a separate import for each. This is genuinely still the standard approach for multi-part UI widgets in real component libraries (tabs, accordions, dropdown menus).

Render Props — Letting the Consumer Control Rendering

function Toggle({ children }) { const [on, setOn] = useState(false); const toggle = () => setOn((v) => !v); return children({ on, toggle }); } // usage — the caller decides exactly what to render, Toggle only owns the behavior <Toggle> {({ on, toggle }) => ( <button onClick={toggle}>{on ? "ON" : "OFF"}</button> )} </Toggle>

Here, children isn't JSX at all — it's a function, called with whatever data/behavior Toggle wants to expose, returning the JSX to actually render. This particular shape ("children as a function") is the most common variant of the render props pattern; the same idea also appears as a separately-named prop (render={({ on, toggle }) => ...}) rather than children specifically. Either way, Toggle owns the boolean state and the toggle logic, while the caller decides completely freely what that state should look like on screen — a button, a checkbox, anything.

A custom hook usually does the same job more simply now
The Toggle example above is solving the exact same problem as Intermediate Chapter 4's useToggle hook — reusable toggle behavior, usable from anywhere. A custom hook is generally the more direct modern choice for sharing behavior specifically; render props remain genuinely useful when a component needs to share both behavior and markup structure together (a list component handling virtualization/scrolling, exposing a render prop per row), which a hook alone can't do.

Higher-Order Components — A Function That Returns a Component

function withLoading(Component) { return function WithLoadingWrapper({ isLoading, ...props }) { if (isLoading) { return <p>Loading...</p>; } return <Component {...props} />; }; } const UserProfileWithLoading = withLoading(UserProfile); // usage <UserProfileWithLoading isLoading={isLoading} user={user} />

A higher-order component is a function taking a component and returning a new component with extra behavior layered on — here, withLoading intercepts the isLoading prop and shows a loading message instead of rendering the wrapped component at all, until it becomes false. This pattern predates hooks entirely, and was the standard way to share cross-cutting behavior (loading states, authentication checks, data injection) before useState/useEffect existed in their current form.

HOCs are largely legacy — prefer a custom hook for new code
Almost everything a HOC like withLoading does, a custom hook does more directly — no wrapper component, no extra layer in the component tree showing up in debugging tools, and no prop-name collisions between what the HOC injects and what the wrapped component already expects. New code today reaches for a custom hook (Intermediate Ch 4) for this kind of cross-cutting concern almost every time; recognizing the HOC pattern still matters for reading and maintaining existing libraries and codebases that predate hooks.
PatternStill common today?Modern alternative
Compound componentsYes — multi-part UI widgets(itself, still the standard approach)
Render propsOccasionally — shared behavior + markup structureA custom hook, when only behavior needs sharing
HOCsRarely in new code — mostly legacyA custom hook, almost always

Coding Challenges

Challenge 1

Build a compound Tabs component family (Tabs, Tabs.Tab, Tabs.Panel) sharing active-tab state via Context internally, with at least three tabs.

📄 View solution
Challenge 2

Build a render-props Toggle component (children as a function exposing { on, toggle }), and use it twice to build two completely different UIs — a button showing ON/OFF, and a checkbox — both reusing the exact same Toggle logic.

📄 View solution
Challenge 3

Build the withLoading HOC from this chapter and use it to wrap a simple component. Then write a useLoading-style custom hook achieving the same "show loading until ready" behavior, and compare the two approaches.

📄 View solution

Chapter 5 Quick Reference

  • Compound components — a family of components sharing implicit state via internal Context (Tabs.Tab, Tabs.Panel)
  • Render props — a prop (often children) that's a function, letting the caller control rendering while the component owns behavior
  • HOCs — a function taking a component, returning a new wrapped component with added behavior
  • Compound components remain the standard for multi-part UI widgets; render props occasionally still earn their place
  • HOCs are largely legacy — a custom hook (Intermediate Ch 4) almost always replaces them in new code
  • Next chapter: performance profiling — measuring before optimizing, with React's own DevTools profiler