Compound components let callers assemble flexible UIs - tabs, cards, form field groups - from named parts that share implicit state. The parent owns activeIndex or isExpanded; children like Tabs.List and Tabs.Panel read it through context instead of prop drilling through every slot.
// Unstable - new object every render re-renders all consumers<TabsContext.Provider value={{ activeIndex, setActiveIndex }}>// Stable - memoize; stabilize setters with useCallback in the rootconst value = useMemo( () => ({ activeIndex, setActiveIndex }), [activeIndex, setActiveIndex]);
Only descendants that read a changed field re-render when context updates.
Split hot and cold context if needed: TabsStateContext + TabsActionsContext for large trees.
For high-frequency updates (animated scrubbers), prefer a hook or ref instead of context.
Missing provider guard - Using Tabs.Tab outside <Tabs> reads null context and silently misbehaves. Fix: Throw in useTabsContext() with a clear error message.
Unstable context value - Inline value={{ activeIndex, setActiveIndex }} re-renders every consumer each parent render. Fix:useMemo the value; wrap setters in useCallback.
Index-based tabs break on reorder - Hard-coded index={2} shifts when marketing inserts a tab. Fix: Key tabs by string id ("account") internally; map to index if needed.
Mounting all panels at once - Heavy charts in hidden tabs still run effects and hurt scroll perf. Fix: Return null from inactive TabPanel or lazy-mount on first activation.
God compound - One mega-<Form> with 20 sub-parts becomes harder than props. Fix: Split into FieldGroup, SelectField, and DateField compounds rather than one export.
Styling inside primitives - Baking brand colors into Tabs.Tab blocks white-label apps. Fix: Accept style / textStyle props or a className hook for NativeWind consumers.
Accessibility state not wired - Tabs without accessibilityRole="tab" and selected state confuse VoiceOver/TalkBack. Fix: Mirror web ARIA roles on Pressable per platform guidance.