A component now sits under five wrappers added one per variation; how do you decide between projecting content, wrapping, and extracting a reusable unit?
answer
- ask what actually varies
- markup, behaviour, or state and effects
- region, wrapper, unit — one each
- growing layer count is the smell
basics
~20 sMatch the tool to what varies: project content when the difference is markup, wrap when the difference is behaviour around a whole subtree, and extract a reusable stateful unit when the difference is state and effects several components need separately.
solid answer
~40 sFive wrappers usually means one tool was used for three jobs, so start by asking what actually differs between the cases. If it is **markup** — a different header, row or empty state — that is a content region on one component, not a wrapper each. If it is **behaviour around a whole subtree** — a gate, instrumentation, error recovery — wrapping is right, but there should be few of those and each should be transparent. If it is **state and effects** several components need separately, extract a reusable unit and call it, because wrapping to inject state forces every consumer to accept an extra tree level. Then pay down the depth: collapse markup-only wrappers into named regions, move state-only wrappers into units, and keep only the genuinely cross-cutting layers.
go deeper
Learn the three tools and what each varies: projected content for markup, a wrapper for behaviour around a subtree, a reusable unit for state and effects.
Justify a choice out loud by naming what differs between the cases, and recognise the smell of a wrapper whose body only rearranges markup around its child.
Diagnose an over-wrapped tree and give a refactor order, saying which layers you keep and why, and what the forwarding debt of each removed layer was costing.
Own it as a codebase rule: how variation gets published, how many pass-through layers are tolerated, and who reviews a new wrapper before it becomes the default way to extend anything.
## Three tools, three kinds of variation Composition without inheritance leaves a component author three moves, and the mistake behind a five-deep wrapper stack is nearly always using one of them for another's job. | Tool | What the caller can vary | Who owns behaviour and state | Cost per use | |---|---|---|---| | Projected content (default, named, scoped) | markup, per region | the receiving component | one more published region | | Wrapper component | behaviour around a whole subtree | the wrapper | one more tree level and forwarding contract | | Reusable stateful unit | nothing visual; state and effects are reused | the calling component instance | a call site and an API to keep stable | So the rule reads: **project content when the difference is markup, wrap when the difference is behaviour that applies to a whole subtree, extract a unit when the difference is state and effects several components need separately.** ## Reading the symptom - A wrapper whose body only rearranges markup around the child — the difference was markup. Publish a named region and delete the wrapper. - A wrapper that exists to supply state to exactly one child — the difference was state. Extract a unit the child calls, and the tree level disappears. - Near-identical blocks repeated by every caller, each re-implementing the same behaviour — the duplication is behaviour, so a wrapper or a unit belongs where the repetition is. - A scoped slot with eight arguments — the component is handing out its internals, which usually means it is doing two jobs. - One component with an input per caller — variation that belonged in the callers' markup was pulled inside instead. ## What wrapper depth actually costs 1. **Forwarding debt.** Every layer must pass through inputs, events, content regions and element handles. The chance all five do it correctly is the product of five chances. 2. **Namespace pressure.** Each layer's own inputs can shadow the inner component's, and the collision surfaces as a silent no-op rather than an error. 3. **Debuggability.** Anyone reading the tree, a stack trace or a failing test walks five frames of nothing before reaching the component that renders something. 4. **Ownership fog.** With five layers, the answer to where a new behaviour goes becomes a matter of taste, and layer six arrives next sprint. 5. **Order sensitivity.** Layers that make decisions — a gate, a boundary, an adapter — depend on their relative order, and that order is invisible at the call site. ## A refactor path 1. List the layers and write down, for each, what varies because of it: markup, behaviour, or state. 2. Collapse every markup-only layer into named regions on the innermost component; callers that used those wrappers fill the regions directly. 3. Convert every state-supplying layer into a unit called by the component that needed the state. 4. Keep the genuinely cross-cutting layers — access, error recovery, instrumentation, an adapter around a component you cannot edit — and make each transparent and independently orderable. 5. Re-check the innermost component's inputs afterwards: if collapsing produced a long list of flags, some of those cases should have stayed regions. ## When deep wrapping is still the right answer - **Cross-cutting decisions.** A gate or an error boundary has no markup opinion and applies uniformly to any subtree, so one implementation serves the whole app and nesting it stays easy to reason about. - **Third-party components.** You cannot publish a region inside a component you do not own, so an adapter layer is the only tool available. - **Route or feature boundaries.** A layer per boundary is a small, fixed number that maps to something real in the product rather than to the number of callers. The distinction is **fixed set versus growing set**. Layers whose count is bounded by architecture are fine. Layers whose count grows with the number of callers are a smell, whatever they are named. ## The two escapes that are not answers - **Inheriting from the component** to override its markup. This couples the subclass to internals that will change, and it is the exact problem composition exists to avoid. - **One large component** with a boolean per caller. The flags multiply combinatorially, nobody tests the combinations, and the component's logic starts branching on who is calling rather than on its data. Between those extremes the three tools cover the ground: markup varies at a region, behaviour varies at a wrapper, state and effects vary at a unit. Naming which one you are choosing, and why, is most of the design taste an interviewer is listening for.
- Which wrapper layers are worth keeping however deep the tree gets?The genuinely cross-cutting ones: access gates, error boundaries, instrumentation, and adapters around a component you cannot edit. They make the same decision for any subtree and hold no markup opinion, so one implementation serves every caller. Layers that arrange markup or supply state should become regions or units instead.
- How do you tell a markup difference from a behaviour difference when both look like a different header?Ask whether the receiving component has to decide anything about it. If it only positions the markup, the difference is markup and belongs in a published region. If it must branch, subscribe, gate or clean something up, it is behaviour, and that belongs in a unit or a wrapper.
- What goes wrong if you collapse the wrappers into one component with an input per variation?The component accumulates flags whose combinations nobody tested, and its internals start branching on callers rather than on data. Content regions keep the variation in the caller's markup where the combinations are explicit, and keep the component's own logic about one thing.
saying these in an interview costs you the question
- Adds a wrapper for every new variation and calls that composition
- Uses a wrapper purely to inject state instead of extracting a unit
- Reaches for component inheritance once the wrappers get deep
- Collapses everything into one component with a boolean per caller
- Counts only visual nesting and ignores the contracts each layer must forward
- Treats a scoped slot and a reusable unit as interchangeable tools