In a React compound component such as `<Tabs><TabList><Tab id="a"/></TabList></Tabs>`, how does a `<Tab>` nested several levels deep learn which tab is selected, and what happens if someone renders `<Tab>` outside any `<Tabs>`?
answer
- the parts are not always direct children
- no prop threading between the parts
- one provider, the whole subtree
- outside the parent you get the default
- guard hook that throws by name
basics
~20 sThe parent publishes the shared state on a React context and each part reads it with useContext, so nesting depth is irrelevant. A part rendered outside the parent silently gets the context default instead, so make the part's hook throw a named error.
solid answer
~50 s`Tabs` creates a context, holds the selected id in state, and renders its children inside the provider; `Tab` and `TabPanel` call `useContext(TabsContext)` to read the current value and the select callback. Because context flows through the entire subtree, the parts keep working at any depth — inside a styling `div`, inside another component, behind a `map` — which is exactly why an implementation that injects props into direct children instead breaks the moment a consumer wraps one. The tradeoff is that the coupling exists only at runtime: outside a `Tabs`, `useContext` simply returns whatever default was passed to `createContext`, so the part renders with `undefined` state instead of failing loudly. I create the context with no usable default and read it through a small hook that throws `useTabsContext must be used inside <Tabs>`, which turns a confusing downstream crash into a message that names the fix.
go deeper
Know that the shared value comes from a context created by the parent, and that each part reads it with useContext rather than receiving it as a prop from the consumer.
Explain that context reaches the whole provider subtree, so parts keep working when wrapped or extracted, and describe exactly what a part sees when no provider is above it.
Show how you make the implicit contract fail loudly: no usable default, a guard that names the offending component, and an error message the consumer can act on without reading library source.
Decide what the context is allowed to expose, because every field in it becomes a contract that existing and future parts depend on and that you cannot change quietly.
## The mechanism The parent creates a context once, at module scope, keeps the widget's state, and renders its children inside the provider. Each part reads that context. In React 19 a context object can be rendered directly as the provider; `<TabsContext.Provider>` still works and is identical in effect. ```jsx const TabsContext = createContext(null); function Tabs({ defaultValue, children }) { const [value, setValue] = useState(defaultValue); const ctx = useMemo(() => ({ value, select: setValue }), [value]); return <TabsContext value={ctx}>{children}</TabsContext>; } function Tab({ id, children }) { const { value, select } = useContext(TabsContext); return ( <button role="tab" aria-selected={value === id} onClick={() => select(id)}> {children} </button> ); } ``` The consumer writes `<Tabs><TabList><Tab id="a">A</Tab></TabList></Tabs>` and never mentions the selected value. That absence is the whole point of the pattern: the coupling between the parts is real but implicit. ## Why depth is the deciding property Context is read by position in the rendered tree, not by parent-child relationship in JSX. Any component rendered anywhere beneath the provider sees the value — one level down, ten levels down, inside a component from a different file, inside a `map`. That is what makes the pattern survive contact with real applications, because consumers *will* wrap the parts: ```jsx <Tabs defaultValue="a"> <TabList> <div className="tab-row"> <Tab id="a">A</Tab> </div> </TabList> </Tabs> ``` An implementation that instead walks the parent's immediate children and injects props into them appears to work in the demo and then fails here, because the wrapper `div` is now the child and the `Tab` is out of reach. Context has no such restriction, which is why it is the mechanism this pattern is built on. ## What a missing provider does React has no concept of "this component requires that provider". A part rendered with no matching provider above it is perfectly legal; `useContext` returns the default argument given to `createContext`. So: - `createContext(null)` → the part receives `null` and typically crashes on the first property access, at a line that has nothing to do with the real mistake. - `createContext({})` or a fabricated default → worse. The part renders, silently, in a permanently wrong state. Nobody gets an error; someone gets a bug report saying the tabs do not switch. This is the discoverability cost of implicit coupling, in its most concrete form. The structure that the API relies on is not expressible in the type of the parent's `children`, so it cannot be checked before the code runs. ## Making it fail loudly The standard containment is a small reader that converts a missing provider into a message naming both the part and the required parent: ```jsx function useTabsContext(componentName) { const ctx = useContext(TabsContext); if (ctx === null) { throw new Error(`<${componentName}> must be rendered inside <Tabs>`); } return ctx; } ``` Two details matter. First, give the context a default that is unmistakably "absent" — `null` or `undefined` — rather than a plausible-looking object, so the check is reliable. Second, name the component in the message: the consumer's stack trace shows library internals, and the one thing they need is which element of theirs is in the wrong place. ## Nested instances of the same widget Providers nest, and the nearest one wins. Two `Tabs` inside each other therefore work without any special support: the inner `Tabs` renders its own provider, so parts inside it read the inner selection while parts outside it read the outer one. What the pattern cannot prevent is a consumer rendering a part belonging to the *outer* widget inside the *inner* subtree — it will quietly join the inner one. That is not a bug you can detect from inside the library, and it is a fair thing to say out loud in an interview: implicit coupling means some misuse is simply undetectable. ## What belongs in the context value Keep it to the resolved state the parts need and stable callbacks — a current value, a select function, generated ids. Do not push the parent's whole props object in: everything you expose becomes a contract the parts and every future part depend on, and a wide context value makes it impossible to change the parent's internals without auditing every part. Build the object with `useMemo` so it is not a fresh identity on every parent render.
- What happens if a consumer nests one `<Tabs>` inside another — does the inner one steal the outer selection?No. Providers nest and each part reads the nearest one above it, so the inner `Tabs` renders its own provider and its parts see the inner value. The case you cannot defend against is a part that logically belongs to the outer widget being placed inside the inner subtree; it will silently join the inner one, which is the honest limit of an implicit API.
- What should the context value actually carry so the parts stay decoupled?The resolved current value, stable callbacks, and any generated ids the parts need for their aria attributes — nothing more. Every field you expose becomes a contract that future parts depend on, so a context stuffed with the parent's props freezes its internals. Memoize the object so the parts are not re-rendered by a fresh identity on every parent render.
saying these in an interview costs you the question
- Thinks the parts must be direct children of the parent
- Says React passes the parent's state down automatically
- Expects React to throw when a provider is missing
- Gives the context a plausible fake default value
- Claims context only reaches one level down