skip to content

You maintain a shared React component library. How do you decide which widgets are worth exposing as compound components, and how do you contain the costs of an implicitly coupled API once you ship one?

level: principalimportance: nice to knowfreq 25%

answer

  1. does the arrangement actually vary?
  2. prop explosion is the signal
  3. fixed shape means fixed API
  4. parent owns anything correctness depends on
  5. each exported part is public surface

basics

~20 s

Give a compound API to widgets whose arrangement genuinely varies across product surfaces, and keep a fixed-structure API where correctness or accessibility depends on that arrangement. Contain the cost with loud runtime guards, worked examples, and an opinionated preset built on the same parts.

solid answer

~50 s

The test I apply is whether the arrangement is a real product variable. If consumers keep asking for one more `render*` or `className` prop, the structure varies and a compound API pays for itself; if the widget only makes sense in one shape — a confirm dialog, a field with its label and error — the flexibility buys nothing and moves correctness into the consumer's hands. When I do ship one, I contain the implicit coupling three ways: the parent owns everything correctness depends on, including ids generated with `useId` for the aria wiring, so misuse cannot silently break accessibility; every part throws a named error when its context is missing; and I ship an opinionated preset composed from the same parts, so the common case stays one component and the compound API is the escape hatch. The strategic cost is versioning — each exported part is public surface, and moving behaviour between parts is a breaking change even when the rendered output is identical.

go deeper

for a junior

You will not be making this call yet, but know that the same widget can be exposed as one component or as a set of parts, and that the choice is deliberate rather than a matter of style.

for a middle

Be able to name the signal that a config-prop API is failing — a growing list of render and class-name props whose only purpose is to let consumers influence layout.

for a senior

Argue the decision on real costs: support burden, accessibility that must not depend on how the consumer composed the parts, and the runtime guards that make misuse legible.

for a principal

Own the consequences across versions and teams: exported parts are frozen public surface, and a preset composed from those same parts is how you keep consistency without taking the flexibility away.

## The decision, stated as a test A compound API is not a maturity level; it is a trade of static guarantees for structural freedom. So the decision is: **is the structure a variable in our product, or a constant we happen to be exposing?** Signals that it is a variable: - The backlog for the component is mostly layout escape hatches — `renderHeader`, `footerClassName`, `slotAfterTitle`. Each of those is a consumer trying to write JSX through a prop. - Teams have forked the component to change its markup. - Real usages differ in *order or nesting*, not just in data. Signals that it is a constant: - The widget has a correctness or accessibility contract that depends on the arrangement — a menu whose items must be children of the menu for keyboard navigation, a field whose error must be associated with its input. - Every usage looks the same and the differences are all data. - The component is internal to one product area with a handful of call sites; the flexibility will never be exercised, and you have only bought yourself an unenforced contract. ## Where flexibility is actively wrong The hardest version of this is accessibility. If the aria relationships depend on how the consumer arranged the parts, you have distributed a correctness requirement to people who will not read the spec. The mitigation is not to refuse the pattern but to keep the wiring in the parent: generate ids with `useId` in the parent, hand each part the id it needs and the id of its partner through context, and have the parts render their own attributes. Then a consumer can nest, wrap and reorder freely and the relationships still hold. What remains unenforceable — a tab with no panel, a menu with no items — becomes a development-time error, not a silent defect. ## Containing the cost **Fail loudly.** Every part reads the context through a guard that throws a message naming the part and the parent it needs. A stack trace through library internals is useless to a consumer; `<TabPanel> must be rendered inside <Tabs>` is actionable. **Ship a preset.** Build the compound parts as the primitive layer, then export an opinionated component composed from them that covers the common case in one element. Document the preset first. This is the single most effective control, because it makes the flexible API opt-in: teams that need a different structure drop down deliberately, everyone else gets consistency for free, and you can fix the common case centrally. **Make examples the specification.** The types cannot express which arrangements are legal, so the docs must. One runnable example per supported arrangement, kept in the same repo as the component and rendered in the component gallery, is worth more than paragraphs of prose. An arrangement with no example is one you have not committed to supporting. **Keep the context narrow.** Everything the context exposes is depended on by the parts and by anything consumers build against them. A wide context value freezes the parent's internals. ## The versioning consequence This is the part that separates a principal answer from a senior one. A config-prop component has one public surface: its props. You can restructure everything inside it — merge internal components, move state, change the DOM — as long as the props and the rendered semantics hold. A compound component publishes its *seams*. Consumers depend on each part existing, on its name, on it being placeable anywhere in the subtree, and often on its DOM output because they style it. Consolidating two parts, moving state from the parent into a part, or requiring a part to be a direct child are all breaking changes, even when the default rendering is byte-identical. Ship a compound API only for widgets whose decomposition you are confident about, because you are freezing that decomposition. The practical hedge is to expose the fewest parts that make the flexibility real. Four parts you are sure about beat nine that mirror your current internals. ## The organizational angle With forty consuming teams, a compound API will produce forty arrangements unless you steer. Treat divergence as data: if several teams composed the same non-default arrangement, that is a missing preset variant, not a discipline problem. Add it to the library and migrate them. The library's job is to make the right thing the easy thing; the compound layer exists so that the small number of genuinely unusual cases do not have to fork.

  • How do you stop forty product teams from arranging your compound parts forty different ways?
    Make the preset the documented default and the compound parts the escape hatch, so using them is a deliberate act. Then treat divergence as data: when several teams compose the same non-default arrangement, that is a missing preset variant to add and migrate them to, not a discipline problem to police.
  • What does shipping a compound API do to your ability to change the implementation later?
    It freezes the seam. Consumers depend on each part existing, on its name, and on being able to place it anywhere in the subtree — and often on its DOM, because they style it. Merging two parts or moving state from the parent into a part is a major-version change even when the rendered output is unchanged.
  • How do you document an API whose legal structures the type system cannot express?
    Worked examples become the specification: one runnable example per arrangement you commit to supporting, living beside the component and rendered in the gallery. Back them with development-time errors that name the rule that was broken. An arrangement with no example is one you have not actually promised to keep working.

saying these in an interview costs you the question

  • Ships every widget as compound because it is the modern pattern
  • Assumes types can constrain the children, so misuse is impossible
  • Treats exported parts as internals that are free to rename
  • Leaves accessibility wiring to whatever the consumer composed
  • Adds another render prop instead of reconsidering the API shape

context