Skip to content
Advanced15 min study

Component composition patterns

Share behavior across components with children, render props, and compound components instead of prop-drilling or duplication.

Question progress0 / 10 completed
Start the lesson
Component composition patterns visual explanation

WHAT YOU WILL BE ABLE TO DO

Learning outcomes

  • Explain component composition patterns in plain language.
  • Connect the behavior to the underlying browser or framework model.
  • Implement the core pattern and reason through edge cases.
  • Answer common follow-ups without relying on memorized phrases.

01Explain it simply

Composition means building complex UI by combining smaller components, rather than one component trying to handle every variation through props. children, render props, and compound components — like Tabs.List and Tabs.Panel — are three ways to let a parent component stay flexible about what it renders without knowing the specifics upfront.

One-line definition: Share behavior across components with children, render props, and compound components instead of prop-drilling or duplication.

02Mental model

The core question every pattern answers is: who decides what renders, and who owns the shared state? children lets the parent stay completely agnostic about content. A render prop lets the parent own state and logic while the consumer decides the exact markup. Compound components split into several linked pieces that implicitly share state through context, giving a JSX-composition-friendly API instead of one component with dozens of props.

03Step by step

  • Check whether a growing prop list — renderHeader, renderFooter, showX, showY — is really a composition problem in disguise.
  • Use children when the parent doesn't need to know anything about what's inside.
  • Use a render prop or hook when the parent owns logic and state but the consumer needs to control rendering.
  • Use compound components with context when several related pieces need to share implicit state.
  • Prefer a custom hook over a wrapper component when there's no UI structure to compose, only logic to share.

04Working example

TSX
// Compound component: shared state via context, flexible JSX compositionconst TabsContext = createContext(null);
function Tabs({ children, defaultValue }) {  const [value, setValue] = useState(defaultValue);  return <TabsContext.Provider value={{ value, setValue }}>{children}</TabsContext.Provider>;}Tabs.List = function TabsList({ children }) { return <div role="tablist">{children}</div>; };Tabs.Tab = function Tab({ value: tabValue, children }) {  const { value, setValue } = useContext(TabsContext);  return <button aria-selected={value === tabValue} onClick={() => setValue(tabValue)}>{children}</button>;};

Tabs owns the selected-tab state once; Tabs.List and Tabs.Tab read and update it through context instead of receiving it via props threaded down manually. The consumer composes the pieces freely, reordering or wrapping them without the parent needing new props for every layout variant.

05Where it is used

  • Design-system components like Tabs, Accordion, or Select that have several related sub-parts
  • Sharing data-fetching logic across differently-rendered UIs via a render prop or hook
  • Layout components such as Modal or Card that shouldn't need to know their contents
  • Replacing a component with a dozen boolean or render* props with a composable API

06Common mistakes

  • Growing a component's prop list indefinitely instead of recognizing a composition problem
  • Building compound components without context, forcing awkward manual prop-threading between the pieces
  • Reaching for a render prop when a plain custom hook would share the same logic more simply
  • Over-engineering composition for a component that only ever renders one way in practice

07Interview answer

Frame the choice around who owns state versus who owns markup — that's the actual decision compound components, render props, and children all resolve differently, and naming it shows deeper understanding than listing pattern names.

Why might a custom hook be a better choice than a render-prop component for sharing 'fetch and track loading state' logic across several differently-styled components?

A render prop still wraps the consumer's markup inside a component that controls when and how children render, adding an extra layer to the tree; a custom hook shares the exact same stateful logic without imposing any rendering structure at all, so each consumer's markup stays completely independent.

DDConcept deep dives

Deep dive 1

Composition answers 'who owns state, who owns markup'

Every composition pattern is really a different answer to one question: who decides what renders and who holds the state behind it? children fully delegates rendering to the caller while the wrapper owns nothing about content. A render prop lets the owner keep state and logic internal while exposing rendering control to the caller through a function. Compound components split ownership across several pieces that implicitly share state through context. Recognizing which axis a given UI problem actually varies along is what picks the right pattern instead of reaching for the most familiar one.

  • A growing prop list is often a composition problem wearing a props disguise.
  • The 'right' pattern depends on whether markup or state is the thing that needs to vary.
  • Compound components and render props both solve the same underlying problem with different ergonomics.

Deep dive 2

Compound components trade explicit props for implicit context coupling

Splitting a widget like Tabs into Tabs, Tabs.List, and Tabs.Tab lets each sub-component read and update shared state through context instead of every piece receiving it via props threaded down manually — this gives a natural, freely-composable JSX API. The trade-off is that the pieces are now implicitly coupled through that shared context rather than an explicit prop contract, so a sub-component rendered outside its parent's provider fails in a way that isn't visible from its own props alone, and needs a deliberate guard to fail helpfully instead of silently.

  • Context is what lets the sub-components share state without prop drilling.
  • The implicit coupling should fail loudly (a thrown error) rather than silently when misused.
  • This pattern fits widgets with several genuinely related, co-dependent pieces — not every component.
TSX
function useTabsContext() {  const ctx = useContext(TabsContext);  if (!ctx) throw new Error('Tabs.Tab must be used inside <Tabs>');  return ctx;}

A guard like this converts a silent, confusing failure (context being null) into an immediate, actionable error at the point of misuse.

Deep dive 3

Hooks share logic without imposing rendering structure

A render-prop component still adds itself as a real node in the component tree, wrapping whatever markup the consumer provides — that's an intentional structural cost when rendering coordination is actually needed. A custom hook shares the exact same stateful logic, subscriptions, or side effects, but adds nothing to the tree at all; the consuming component's own markup stays completely untouched and independent. When the only thing being shared is logic, not rendering behavior, a hook is almost always the simpler choice.

  • A hook and a render-prop component can share identical internal logic with very different ergonomics.
  • Extra wrapper components complicate the rendered tree and dev tools inspection.
  • Prefer the pattern that adds the least structure necessary for what's actually being shared.

QAInterview questions and model answers

Attempt each answer aloud before opening it. The model answer shows the depth and precision expected in an interview; it is not a script to memorize.

Intermediate · Conceptual · 1 min · Question 1What question does every composition pattern ultimately answer?Open model answer

Model answer

Who decides what renders, and who owns the shared state involved. children hands rendering entirely to the caller; a render prop lets the owner keep state while the caller controls markup; compound components share state implicitly across several linked pieces via context.

Open question page →
Intermediate · Coding · 1 min · Question 2What's a concrete sign that a component needs a composition pattern instead of more props?Open model answer

Model answer

A prop list growing with variations like renderHeader, renderFooter, showIconA, showIconB for every new visual variant — each addition requires touching the shared component itself instead of composing existing pieces differently.

JSX
// Smell: every variant adds a prop to the shared component<Card renderHeader={...} renderFooter={...} showBadge dense />
// Composition: the caller assembles the parts it needs<Card><Card.Header>...</Card.Header><Card.Body>...</Card.Body></Card>
Open question page →
Intermediate · Coding · 1 min · Question 3How do compound components typically share state between their pieces?Open model answer

Model answer

Through React context — a parent component like Tabs creates the shared state and a context provider, and its associated sub-components like Tabs.List and Tabs.Tab consume that context instead of receiving the state threaded through explicit props.

JSX
const TabsCtx = createContext(null);function Tabs({ children }) {  const [active, setActive] = useState(0);  return <TabsCtx.Provider value={{ active, setActive }}>{children}</TabsCtx.Provider>;}Tabs.Tab = function Tab({ index, children }) {  const { active, setActive } = useContext(TabsCtx);  return <button aria-selected={active === index} onClick={() => setActive(index)}>{children}</button>;};
Open question page →
Intermediate · Conceptual · 1 min · Question 4When is a custom hook a better choice than a render-prop component?Open model answer

Model answer

When there's stateful logic to share but no rendering structure that needs to wrap the consumer — a hook shares the exact same logic without adding an extra component layer or constraining how the consumer's markup is structured.

Open question page →
Intermediate · Conceptual · 1 min · Question 5What's a downside of the compound-component pattern compared to a single component with props?Open model answer

Model answer

It requires the caller to compose several related pieces correctly, coupling them implicitly through shared context rather than an explicit prop contract — a piece used outside its parent's provider will fail silently or need an explicit guard.

Open question page →
Intermediate · Conceptual · 1 min · Question 6Why does children work well for something like a generic Modal or Card wrapper?Open model answer

Model answer

The wrapper's job is purely structural — providing an overlay, a border, spacing — and has no reason to know or care what content it's wrapping, so children lets any content compose into it without the wrapper needing new props per use case.

Open question page →
Advanced · Conceptual · 1 min · Question 7How does React.Children.only relate to a component using children?Open model answer

Model answer

It's a utility that throws if children isn't exactly one React element, useful when a component's contract genuinely requires a single child, such as a component that clones and augments its one child with extra props.

Open question page →
Intermediate · Conceptual · 1 min · Question 8Can a compound component be made to fail helpfully if a sub-component is used outside its parent's provider?Open model answer

Model answer

Yes — the shared context's default value can be set to undefined or a sentinel, and each sub-component can throw a clear error if it reads that sentinel, rather than silently rendering broken behavior.

Open question page →
Advanced · Conceptual · 1 min · Question 9What is 'prop drilling', and how does context-based composition avoid it?Open model answer

Model answer

Prop drilling is passing a prop down through several intermediate components that don't use it themselves, only to reach a deeply nested consumer; compound components sidestep this by having each piece read shared state directly from context instead of via props.

Open question page →
Advanced · Conceptual · 1 min · Question 10Why might a higher-order component (HOC) be considered legacy compared to a custom hook for sharing logic?Open model answer

Model answer

An HOC wraps a component in another component layer and can create prop naming collisions or obscure the component tree in dev tools; a hook shares the same logic without adding a wrapper component or any indirection in the rendered tree.

Open question page →

SCScenario questions

Scenario 1

An Accordion component has grown to accept renderHeader, renderIcon, allowMultipleOpen, headerClassName, and six other props, and every new design variation adds another prop.

  1. Recognize the prop growth as a composition problem in disguise.
  2. Redesign Accordion as a compound component: Accordion, Accordion.Item, Accordion.Header, Accordion.Panel.
  3. Move shared open/closed state into context owned by the top-level Accordion.
  4. Let each consumer compose exactly the header and panel markup it needs directly in JSX.
Reveal worked answer

I would restructure it as a compound component. The top-level Accordion would own open-item state in context, while Accordion.Item, Accordion.Header, and Accordion.Panel read and update that context directly. Consumers compose exactly the markup they want for each part in JSX instead of passing render functions through props, and a new visual variant becomes a JSX change at the call site rather than a new prop on the shared component.

Verify and go deeper