In a React component library, what does attaching parts as static properties — `Tabs.List = TabList; Tabs.Panel = TabPanel;` — actually give you, and what does it not do?
answer
- it is ordinary JavaScript, not React
- JSX accepts a dotted tag name
- one import, one namespace
- enforces nothing about nesting
- tree-shaking and DevTools names
basics
~20 sIt is plain JavaScript property assignment, and JSX accepts a dotted tag, so <Tabs.Panel /> renders that component. It groups the API under one import and one name; it enforces nothing about where the parts may be used.
solid answer
~40 s`Tabs.List = TabList` is just an assignment on a function object — React never sees it and gives it no meaning. The value is ergonomic: one import, a name that says which widget the part belongs to, and no collision between your `Tab` and someone else's. What it does not do is create any relationship React understands. `<Tabs.Panel />` rendered outside a `<Tabs>` is as legal as any other element, and the enforcement still has to come from the context guard inside the part itself. Two practical costs: bundlers generally tree-shake a bag of properties hanging off one exported object less effectively than separate named exports, and DevTools shows the underlying function's name unless you set `displayName`. Named exports plus `import * as Tabs from './tabs'` gives most of the ergonomics without the object.
go deeper
Say plainly that this is a property assignment on a function object and that JSX allows a dotted tag, so it is packaging rather than behaviour.
Explain what it costs — weaker tree-shaking and a DevTools name you have to set yourself — and name what still has to enforce correct usage at runtime.
Compare it with named exports plus a namespace import, and choose based on how consumers actually import from the library and how much dead code the bundle can afford.
Treat each exposed part as public API: once Tabs.Panel exists, relocating that behaviour or renaming it is a breaking change for every consumer, whatever the internals look like.
## What the assignment is ```jsx function Tabs({ children }) { /* … */ } function TabList({ children }) { /* … */ } function Panel({ id, children }) { /* … */ } Tabs.List = TabList; Tabs.Panel = Panel; ``` Functions are objects in JavaScript, so you can hang properties on them. That is the entire mechanism. There is no registration, no React API involved, and no metadata attached to the component — after those two lines `Tabs` is the same component it was, with two extra properties. ## Why JSX accepts the dot JSX distinguishes an intrinsic element from a component by whether the tag is a lowercase identifier. A dotted tag is neither: it is a member expression, and JSX compiles it to a property lookup on the value in scope. `<Tabs.Panel id="a" />` becomes an element whose type is the result of evaluating `Tabs.Panel`. This is why the same syntax works for a namespace import, where the object is produced by the module system rather than by you. ## What it buys **One import.** `import { Tabs } from './tabs'` brings the whole widget, and a consumer discovers the parts through autocomplete on `Tabs.` — a real advantage, given that discoverability is exactly what a compound API sacrifices. **Namespacing.** `Tabs.Panel` cannot be confused with the `Panel` from your dialog or your sidebar, and the reading of a page of JSX tells you which widget each element belongs to. **A visible grouping in review.** Someone reading the call site sees that `Tabs.List` and `Tabs.Panel` are pieces of one thing, which is information the compound pattern otherwise hides. ## What it does not do It does not enforce nesting. React has no idea that `Tabs.Panel` was reached through `Tabs`; by render time the element's type is simply the `Panel` function. Rendering `<Tabs.Panel />` alone renders `Panel` alone, and whether that is an error is entirely up to `Panel` — in practice, up to the context guard inside it that throws when no provider is above. It does not pass anything from the parent to the part. The dot is a lookup at the *definition* site, not a relationship at the *render* site. Whatever the parts share still travels by context. It does not change types, validation, or ordering. Nothing about the dotted call prevents two lists, a missing panel, or a part from another widget mixed in. ## The costs **Tree-shaking.** When every part is reachable as a property of one exported object, a bundler that sees `import { Tabs }` generally has to keep all of them, because it cannot prove the properties are unused. Separate named exports let a bundler drop the ones nobody imported. For a small widget this is noise; across a large library shipped as one entry point it adds up, and it is the usual reason a library exports the parts individually and *also* attaches them. **Names in DevTools.** The tree shows the component function's own name, so `Tabs.List` appears as `TabList`, and a part defined as an anonymous arrow assigned to a property may show with no useful name at all. Setting `TabList.displayName = 'Tabs.List'` makes the tree read the way the call site does — a small thing that pays for itself the first time you profile a page full of these. **Slightly awkward typing.** In TypeScript the parent's type has to be widened to carry the extra properties, which is why libraries often build the object explicitly rather than mutating the function after the fact. ## The alternative Export the parts as ordinary named exports and let the consumer decide: ```jsx // tabs.js export function Tabs({ children }) { /* … */ } export function TabList({ children }) { /* … */ } export function Panel({ id, children }) { /* … */ } // call site import * as Tabs from './tabs'; <Tabs.Tabs><Tabs.TabList /></Tabs.Tabs>; ``` A namespace import gives the same dotted call sites while keeping each part individually importable and individually droppable. Many libraries ship both: named exports as the real API, static properties as a convenience. Either way the decision is packaging, and it is worth saying plainly in an interview that it has no effect on how the widget behaves.
- How do you make React DevTools show these parts as `Tabs.List` rather than `TabList`?Set `TabList.displayName = 'Tabs.List'`. DevTools falls back to the function's own name, so a part defined as an anonymous arrow assigned straight to a property can appear with no useful name at all — which is the real reason to set it, more than cosmetics in the tree.
- If a consumer imports only `Tabs`, does the bundle include every part attached to it?In practice yes. The parts are reachable as properties of the object that was imported, so a bundler generally cannot prove they are unused and keeps them. Separate named exports let it drop whatever nobody imports, which is why libraries commonly export the parts individually as well as attaching them.
saying these in an interview costs you the question
- Thinks dot notation makes React enforce parent-child nesting
- Believes `<Tabs.Panel />` is special JSX with React semantics
- Says the parts automatically receive the parent's props
- Assumes DevTools shows the dotted name without displayName