[{"data":1,"prerenderedAt":1803},["ShallowReactive",2],{"blog-\u002Fblog\u002Freact-compound-components-guide":3},{"id":4,"title":5,"body":6,"description":1788,"difficulty":1789,"extension":1790,"framework":1791,"frameworkSlug":1792,"meta":1793,"navigation":284,"order":67,"path":1794,"qaPath":1795,"seo":1796,"stem":1797,"subtopic":1798,"topic":1799,"topicSlug":1800,"updated":1801,"__hash__":1802},"blog\u002Fblog\u002Freact-compound-components-guide.md","React Compound Components — Complete Interview Guide",{"type":7,"value":8,"toc":1776},"minimark",[9,14,46,54,234,245,249,252,613,634,638,646,700,707,711,731,928,934,938,960,964,967,1229,1235,1239,1246,1315,1321,1325,1336,1473,1477,1484,1702,1705,1709,1772],[10,11,13],"h2",{"id":12},"the-problem-with-monolithic-component-apis","The Problem with Monolithic Component APIs",[15,16,17,18,22,23,26,27,26,30,26,33,26,36,26,39,26,42,45],"p",{},"Imagine building a ",[19,20,21],"code",{},"\u003CTabs>"," component. The first instinct is to accept everything as props: ",[19,24,25],{},"tabs",", ",[19,28,29],{},"activeTab",[19,31,32],{},"onTabChange",[19,34,35],{},"renderTab",[19,37,38],{},"renderPanel",[19,40,41],{},"tabClassName",[19,43,44],{},"panelClassName","… The list grows with every new consumer. Each new layout requirement — putting a button next to the tab bar, nesting a badge inside a label — means adding yet another prop or escape hatch. The component becomes a configuration object masquerading as a UI element.",[15,47,48,49,53],{},"The ",[50,51,52],"strong",{},"compound components pattern"," solves this by splitting the monolithic component into a family of cooperating sub-components. The parent owns shared state; the sub-components consume it implicitly. The consumer controls layout by composing sub-components in JSX, just like standard HTML elements:",[55,56,61],"pre",{"className":57,"code":58,"language":59,"meta":60,"style":60},"language-jsx shiki shiki-themes github-light github-dark","\u003CTabs defaultValue=\"overview\">\n  \u003CTabs.List>\n    \u003CTabs.Tab value=\"overview\">Overview \u003CBadge count={3} \u002F>\u003C\u002FTabs.Tab>\n    \u003CTabs.Tab value=\"settings\">Settings\u003C\u002FTabs.Tab>\n  \u003C\u002FTabs.List>\n  \u003CTabs.Panel value=\"overview\">\u003COverviewContent \u002F>\u003C\u002FTabs.Panel>\n  \u003CTabs.Panel value=\"settings\">\u003CSettingsContent \u002F>\u003C\u002FTabs.Panel>\n\u003C\u002FTabs>\n","jsx","",[19,62,63,91,102,142,163,173,200,224],{"__ignoreMap":60},[64,65,68,72,76,80,84,88],"span",{"class":66,"line":67},"line",1,[64,69,71],{"class":70},"sVt8B","\u003C",[64,73,75],{"class":74},"sj4cs","Tabs",[64,77,79],{"class":78},"sScJk"," defaultValue",[64,81,83],{"class":82},"szBVR","=",[64,85,87],{"class":86},"sZZnC","\"overview\"",[64,89,90],{"class":70},">\n",[64,92,94,97,100],{"class":66,"line":93},2,[64,95,96],{"class":70},"  \u003C",[64,98,99],{"class":74},"Tabs.List",[64,101,90],{"class":70},[64,103,105,108,111,114,116,118,121,124,127,129,132,135,138,140],{"class":66,"line":104},3,[64,106,107],{"class":70},"    \u003C",[64,109,110],{"class":74},"Tabs.Tab",[64,112,113],{"class":78}," value",[64,115,83],{"class":82},[64,117,87],{"class":86},[64,119,120],{"class":70},">Overview \u003C",[64,122,123],{"class":74},"Badge",[64,125,126],{"class":78}," count",[64,128,83],{"class":82},[64,130,131],{"class":70},"{",[64,133,134],{"class":74},"3",[64,136,137],{"class":70},"} \u002F>\u003C\u002F",[64,139,110],{"class":74},[64,141,90],{"class":70},[64,143,145,147,149,151,153,156,159,161],{"class":66,"line":144},4,[64,146,107],{"class":70},[64,148,110],{"class":74},[64,150,113],{"class":78},[64,152,83],{"class":82},[64,154,155],{"class":86},"\"settings\"",[64,157,158],{"class":70},">Settings\u003C\u002F",[64,160,110],{"class":74},[64,162,90],{"class":70},[64,164,166,169,171],{"class":66,"line":165},5,[64,167,168],{"class":70},"  \u003C\u002F",[64,170,99],{"class":74},[64,172,90],{"class":70},[64,174,176,178,181,183,185,187,190,193,196,198],{"class":66,"line":175},6,[64,177,96],{"class":70},[64,179,180],{"class":74},"Tabs.Panel",[64,182,113],{"class":78},[64,184,83],{"class":82},[64,186,87],{"class":86},[64,188,189],{"class":70},">\u003C",[64,191,192],{"class":74},"OverviewContent",[64,194,195],{"class":70}," \u002F>\u003C\u002F",[64,197,180],{"class":74},[64,199,90],{"class":70},[64,201,203,205,207,209,211,213,215,218,220,222],{"class":66,"line":202},7,[64,204,96],{"class":70},[64,206,180],{"class":74},[64,208,113],{"class":78},[64,210,83],{"class":82},[64,212,155],{"class":86},[64,214,189],{"class":70},[64,216,217],{"class":74},"SettingsContent",[64,219,195],{"class":70},[64,221,180],{"class":74},[64,223,90],{"class":70},[64,225,227,230,232],{"class":66,"line":226},8,[64,228,229],{"class":70},"\u003C\u002F",[64,231,75],{"class":74},[64,233,90],{"class":70},[15,235,236,237,240,241,244],{},"The consumer inserted a ",[19,238,239],{},"\u003CBadge>"," inside a tab label without any prop. The tab bar and panels can be separated by arbitrary elements. This is the key benefit: ",[50,242,243],{},"layout flexibility without API sprawl",".",[10,246,248],{"id":247},"sharing-state-with-context","Sharing State with Context",[15,250,251],{},"The mechanism that makes this work is a private Context. The parent component creates it, provides the shared state, and keeps the Context object unexported — it is an internal implementation detail. Sub-components reach into the Context to read what they need.",[55,253,255],{"className":57,"code":254,"language":59,"meta":60,"style":60},"const TabsContext = createContext(null);\n\nfunction useTabs() {\n  const ctx = useContext(TabsContext);\n  if (!ctx) throw new Error('\u003CTabs.Tab> must be rendered inside \u003CTabs>');\n  return ctx;\n}\n\nfunction Tabs({ defaultValue, children }) {\n  const [active, setActive] = useState(defaultValue);\n  return (\n    \u003CTabsContext.Provider value={{ active, setActive }}>\n      {children}\n    \u003C\u002FTabsContext.Provider>\n  );\n}\n\nTabs.Tab = function TabsTab({ value, children }) {\n  const { active, setActive } = useTabs();\n  return (\n    \u003Cbutton\n      role=\"tab\"\n      aria-selected={active === value}\n      onClick={() => setActive(value)}\n    >\n      {children}\n    \u003C\u002Fbutton>\n  );\n};\n",[19,256,257,280,286,297,313,343,351,356,360,383,410,418,433,439,449,455,460,465,493,517,524,533,544,561,581,587,592,602,607],{"__ignoreMap":60},[64,258,259,262,265,268,271,274,277],{"class":66,"line":67},[64,260,261],{"class":82},"const",[64,263,264],{"class":74}," TabsContext",[64,266,267],{"class":82}," =",[64,269,270],{"class":78}," createContext",[64,272,273],{"class":70},"(",[64,275,276],{"class":74},"null",[64,278,279],{"class":70},");\n",[64,281,282],{"class":66,"line":93},[64,283,285],{"emptyLinePlaceholder":284},true,"\n",[64,287,288,291,294],{"class":66,"line":104},[64,289,290],{"class":82},"function",[64,292,293],{"class":78}," useTabs",[64,295,296],{"class":70},"() {\n",[64,298,299,302,305,307,310],{"class":66,"line":144},[64,300,301],{"class":82},"  const",[64,303,304],{"class":74}," ctx",[64,306,267],{"class":82},[64,308,309],{"class":78}," useContext",[64,311,312],{"class":70},"(TabsContext);\n",[64,314,315,318,321,324,327,330,333,336,338,341],{"class":66,"line":165},[64,316,317],{"class":82},"  if",[64,319,320],{"class":70}," (",[64,322,323],{"class":82},"!",[64,325,326],{"class":70},"ctx) ",[64,328,329],{"class":82},"throw",[64,331,332],{"class":82}," new",[64,334,335],{"class":78}," Error",[64,337,273],{"class":70},[64,339,340],{"class":86},"'\u003CTabs.Tab> must be rendered inside \u003CTabs>'",[64,342,279],{"class":70},[64,344,345,348],{"class":66,"line":175},[64,346,347],{"class":82},"  return",[64,349,350],{"class":70}," ctx;\n",[64,352,353],{"class":66,"line":202},[64,354,355],{"class":70},"}\n",[64,357,358],{"class":66,"line":226},[64,359,285],{"emptyLinePlaceholder":284},[64,361,363,365,368,371,375,377,380],{"class":66,"line":362},9,[64,364,290],{"class":82},[64,366,367],{"class":78}," Tabs",[64,369,370],{"class":70},"({ ",[64,372,374],{"class":373},"s4XuR","defaultValue",[64,376,26],{"class":70},[64,378,379],{"class":373},"children",[64,381,382],{"class":70}," }) {\n",[64,384,386,388,391,394,396,399,402,404,407],{"class":66,"line":385},10,[64,387,301],{"class":82},[64,389,390],{"class":70}," [",[64,392,393],{"class":74},"active",[64,395,26],{"class":70},[64,397,398],{"class":74},"setActive",[64,400,401],{"class":70},"] ",[64,403,83],{"class":82},[64,405,406],{"class":78}," useState",[64,408,409],{"class":70},"(defaultValue);\n",[64,411,413,415],{"class":66,"line":412},11,[64,414,347],{"class":82},[64,416,417],{"class":70}," (\n",[64,419,421,423,426,428,430],{"class":66,"line":420},12,[64,422,107],{"class":70},[64,424,425],{"class":74},"TabsContext.Provider",[64,427,113],{"class":78},[64,429,83],{"class":82},[64,431,432],{"class":70},"{{ active, setActive }}>\n",[64,434,436],{"class":66,"line":435},13,[64,437,438],{"class":70},"      {children}\n",[64,440,442,445,447],{"class":66,"line":441},14,[64,443,444],{"class":70},"    \u003C\u002F",[64,446,425],{"class":74},[64,448,90],{"class":70},[64,450,452],{"class":66,"line":451},15,[64,453,454],{"class":70},"  );\n",[64,456,458],{"class":66,"line":457},16,[64,459,355],{"class":70},[64,461,463],{"class":66,"line":462},17,[64,464,285],{"emptyLinePlaceholder":284},[64,466,468,471,474,476,479,482,484,487,489,491],{"class":66,"line":467},18,[64,469,470],{"class":70},"Tabs.",[64,472,473],{"class":78},"Tab",[64,475,267],{"class":82},[64,477,478],{"class":82}," function",[64,480,481],{"class":78}," TabsTab",[64,483,370],{"class":70},[64,485,486],{"class":373},"value",[64,488,26],{"class":70},[64,490,379],{"class":373},[64,492,382],{"class":70},[64,494,496,498,501,503,505,507,510,512,514],{"class":66,"line":495},19,[64,497,301],{"class":82},[64,499,500],{"class":70}," { ",[64,502,393],{"class":74},[64,504,26],{"class":70},[64,506,398],{"class":74},[64,508,509],{"class":70}," } ",[64,511,83],{"class":82},[64,513,293],{"class":78},[64,515,516],{"class":70},"();\n",[64,518,520,522],{"class":66,"line":519},20,[64,521,347],{"class":82},[64,523,417],{"class":70},[64,525,527,529],{"class":66,"line":526},21,[64,528,107],{"class":70},[64,530,532],{"class":531},"s9eBZ","button\n",[64,534,536,539,541],{"class":66,"line":535},22,[64,537,538],{"class":78},"      role",[64,540,83],{"class":82},[64,542,543],{"class":86},"\"tab\"\n",[64,545,547,550,552,555,558],{"class":66,"line":546},23,[64,548,549],{"class":78},"      aria-selected",[64,551,83],{"class":82},[64,553,554],{"class":70},"{active ",[64,556,557],{"class":82},"===",[64,559,560],{"class":70}," value}\n",[64,562,564,567,569,572,575,578],{"class":66,"line":563},24,[64,565,566],{"class":78},"      onClick",[64,568,83],{"class":82},[64,570,571],{"class":70},"{() ",[64,573,574],{"class":82},"=>",[64,576,577],{"class":78}," setActive",[64,579,580],{"class":70},"(value)}\n",[64,582,584],{"class":66,"line":583},25,[64,585,586],{"class":70},"    >\n",[64,588,590],{"class":66,"line":589},26,[64,591,438],{"class":70},[64,593,595,597,600],{"class":66,"line":594},27,[64,596,444],{"class":70},[64,598,599],{"class":531},"button",[64,601,90],{"class":70},[64,603,605],{"class":66,"line":604},28,[64,606,454],{"class":70},[64,608,610],{"class":66,"line":609},29,[64,611,612],{"class":70},"};\n",[15,614,615,616,320,619,622,623,626,627,629,630,633],{},"Wrapping the Context in a ",[50,617,618],{},"custom hook with a guard",[19,620,621],{},"useTabs",") is a best practice. When a ",[19,624,625],{},"\u003CTabs.Tab>"," is rendered outside a ",[19,628,21],{},", it throws a clear error message instead of silently returning ",[19,631,632],{},"undefined"," and causing a cryptic crash downstream.",[10,635,637],{"id":636},"the-dot-notation-api","The Dot-Notation API",[15,639,640,641,26,643,645],{},"Attaching sub-components as static properties on the parent (",[19,642,110],{},[19,644,180],{},") groups the whole family under one import and signals their relationship visually in JSX. This is the dominant style in design system libraries like Radix UI, Headless UI, and Mantine.",[55,647,649],{"className":57,"code":648,"language":59,"meta":60,"style":60},"Tabs.List  = TabsList;\nTabs.Tab   = TabsTab;\nTabs.Panel = TabsPanel;\n\nexport default Tabs; \u002F\u002F one import, four components\n",[19,650,651,661,671,681,685],{"__ignoreMap":60},[64,652,653,656,658],{"class":66,"line":67},[64,654,655],{"class":70},"Tabs.List  ",[64,657,83],{"class":82},[64,659,660],{"class":70}," TabsList;\n",[64,662,663,666,668],{"class":66,"line":93},[64,664,665],{"class":70},"Tabs.Tab   ",[64,667,83],{"class":82},[64,669,670],{"class":70}," TabsTab;\n",[64,672,673,676,678],{"class":66,"line":104},[64,674,675],{"class":70},"Tabs.Panel ",[64,677,83],{"class":82},[64,679,680],{"class":70}," TabsPanel;\n",[64,682,683],{"class":66,"line":144},[64,684,285],{"emptyLinePlaceholder":284},[64,686,687,690,693,696],{"class":66,"line":165},[64,688,689],{"class":82},"export",[64,691,692],{"class":82}," default",[64,694,695],{"class":70}," Tabs; ",[64,697,699],{"class":698},"sJ8bj","\u002F\u002F one import, four components\n",[15,701,702,703,706],{},"An alternative is named exports (",[19,704,705],{},"export { Tabs, TabsList, TabsTab }","), which works well with tree-shaking but loses the visual grouping at the import site. For shared design system components, dot-notation wins; for internal app components, named exports are fine.",[10,708,710],{"id":709},"controlled-vs-uncontrolled-modes","Controlled vs Uncontrolled Modes",[15,712,713,714,717,718,720,721,717,724,726,727,730],{},"Good compound components support both usage modes, mirroring native HTML inputs. In ",[50,715,716],{},"uncontrolled"," mode the caller passes ",[19,719,374],{}," and the component manages state internally. In ",[50,722,723],{},"controlled",[19,725,486],{}," and ",[19,728,729],{},"onChange"," and owns the state.",[55,732,734],{"className":57,"code":733,"language":59,"meta":60,"style":60},"function Tabs({ value, defaultValue, onChange, children }) {\n  const [internal, setInternal] = useState(defaultValue ?? null);\n  const isControlled = value !== undefined;\n  const active = isControlled ? value : internal;\n\n  function handleChange(next) {\n    if (!isControlled) setInternal(next);\n    onChange?.(next);\n  }\n\n  return (\n    \u003CTabsContext.Provider value={{ active, handleChange }}>\n      {children}\n    \u003C\u002FTabsContext.Provider>\n  );\n}\n",[19,735,736,760,791,812,835,839,855,872,880,885,889,895,908,912,920,924],{"__ignoreMap":60},[64,737,738,740,742,744,746,748,750,752,754,756,758],{"class":66,"line":67},[64,739,290],{"class":82},[64,741,367],{"class":78},[64,743,370],{"class":70},[64,745,486],{"class":373},[64,747,26],{"class":70},[64,749,374],{"class":373},[64,751,26],{"class":70},[64,753,729],{"class":373},[64,755,26],{"class":70},[64,757,379],{"class":373},[64,759,382],{"class":70},[64,761,762,764,766,769,771,774,776,778,780,783,786,789],{"class":66,"line":93},[64,763,301],{"class":82},[64,765,390],{"class":70},[64,767,768],{"class":74},"internal",[64,770,26],{"class":70},[64,772,773],{"class":74},"setInternal",[64,775,401],{"class":70},[64,777,83],{"class":82},[64,779,406],{"class":78},[64,781,782],{"class":70},"(defaultValue ",[64,784,785],{"class":82},"??",[64,787,788],{"class":74}," null",[64,790,279],{"class":70},[64,792,793,795,798,800,803,806,809],{"class":66,"line":104},[64,794,301],{"class":82},[64,796,797],{"class":74}," isControlled",[64,799,267],{"class":82},[64,801,802],{"class":70}," value ",[64,804,805],{"class":82},"!==",[64,807,808],{"class":74}," undefined",[64,810,811],{"class":70},";\n",[64,813,814,816,819,821,824,827,829,832],{"class":66,"line":144},[64,815,301],{"class":82},[64,817,818],{"class":74}," active",[64,820,267],{"class":82},[64,822,823],{"class":70}," isControlled ",[64,825,826],{"class":82},"?",[64,828,802],{"class":70},[64,830,831],{"class":82},":",[64,833,834],{"class":70}," internal;\n",[64,836,837],{"class":66,"line":165},[64,838,285],{"emptyLinePlaceholder":284},[64,840,841,844,847,849,852],{"class":66,"line":175},[64,842,843],{"class":82},"  function",[64,845,846],{"class":78}," handleChange",[64,848,273],{"class":70},[64,850,851],{"class":373},"next",[64,853,854],{"class":70},") {\n",[64,856,857,860,862,864,867,869],{"class":66,"line":202},[64,858,859],{"class":82},"    if",[64,861,320],{"class":70},[64,863,323],{"class":82},[64,865,866],{"class":70},"isControlled) ",[64,868,773],{"class":78},[64,870,871],{"class":70},"(next);\n",[64,873,874,877],{"class":66,"line":226},[64,875,876],{"class":78},"    onChange",[64,878,879],{"class":70},"?.(next);\n",[64,881,882],{"class":66,"line":362},[64,883,884],{"class":70},"  }\n",[64,886,887],{"class":66,"line":385},[64,888,285],{"emptyLinePlaceholder":284},[64,890,891,893],{"class":66,"line":412},[64,892,347],{"class":82},[64,894,417],{"class":70},[64,896,897,899,901,903,905],{"class":66,"line":420},[64,898,107],{"class":70},[64,900,425],{"class":74},[64,902,113],{"class":78},[64,904,83],{"class":82},[64,906,907],{"class":70},"{{ active, handleChange }}>\n",[64,909,910],{"class":66,"line":435},[64,911,438],{"class":70},[64,913,914,916,918],{"class":66,"line":441},[64,915,444],{"class":70},[64,917,425],{"class":74},[64,919,90],{"class":70},[64,921,922],{"class":66,"line":451},[64,923,454],{"class":70},[64,925,926],{"class":66,"line":457},[64,927,355],{"class":70},[15,929,930,931,933],{},"Never switch between modes at runtime — if ",[19,932,486],{}," goes from defined to undefined, log a warning (React's controlled input warning does exactly this).",[10,935,937],{"id":936},"the-reactchildren-approach-legacy","The React.Children Approach (Legacy)",[15,939,940,941,944,945,948,949,952,953,955,956,959],{},"Before Context became ergonomic, a common technique was ",[19,942,943],{},"React.Children.map"," + ",[19,946,947],{},"React.cloneElement"," to inject state as props into direct children. Interviewers ask about this to test historical knowledge. Know its fatal flaw: it only reaches ",[50,950,951],{},"direct children",". Wrapping a ",[19,954,625],{}," in a ",[19,957,958],{},"\u003Cdiv>"," for layout breaks the injection chain entirely. Prefer Context for all new compound components.",[10,961,963],{"id":962},"typescript-typing","TypeScript Typing",[15,965,966],{},"Three things need types: the Context shape, each sub-component's props, and the parent augmented with its sub-component statics.",[55,968,972],{"className":969,"code":970,"language":971,"meta":60,"style":60},"language-tsx shiki shiki-themes github-light github-dark","interface TabsContextValue {\n  active: string;\n  handleChange: (v: string) => void;\n}\n\ninterface TabsComponent extends React.FC\u003CTabsProps> {\n  List:  React.FC\u003C{ children: React.ReactNode }>;\n  Tab:   React.FC\u003C{ value: string; children: React.ReactNode }>;\n  Panel: React.FC\u003C{ value: string; children: React.ReactNode }>;\n}\n\nconst Tabs = function Tabs(props: TabsProps) { \u002F* ... *\u002F } as TabsComponent;\nTabs.List  = TabsList;\nTabs.Tab   = TabsTab;\nTabs.Panel = TabsPanel;\n","tsx",[19,973,974,985,997,1023,1027,1031,1057,1088,1125,1160,1164,1168,1205,1213,1221],{"__ignoreMap":60},[64,975,976,979,982],{"class":66,"line":67},[64,977,978],{"class":82},"interface",[64,980,981],{"class":78}," TabsContextValue",[64,983,984],{"class":70}," {\n",[64,986,987,990,992,995],{"class":66,"line":93},[64,988,989],{"class":373},"  active",[64,991,831],{"class":82},[64,993,994],{"class":74}," string",[64,996,811],{"class":70},[64,998,999,1002,1004,1006,1009,1011,1013,1016,1018,1021],{"class":66,"line":104},[64,1000,1001],{"class":78},"  handleChange",[64,1003,831],{"class":82},[64,1005,320],{"class":70},[64,1007,1008],{"class":373},"v",[64,1010,831],{"class":82},[64,1012,994],{"class":74},[64,1014,1015],{"class":70},") ",[64,1017,574],{"class":82},[64,1019,1020],{"class":74}," void",[64,1022,811],{"class":70},[64,1024,1025],{"class":66,"line":144},[64,1026,355],{"class":70},[64,1028,1029],{"class":66,"line":165},[64,1030,285],{"emptyLinePlaceholder":284},[64,1032,1033,1035,1038,1041,1044,1046,1049,1051,1054],{"class":66,"line":175},[64,1034,978],{"class":82},[64,1036,1037],{"class":78}," TabsComponent",[64,1039,1040],{"class":82}," extends",[64,1042,1043],{"class":78}," React",[64,1045,244],{"class":70},[64,1047,1048],{"class":78},"FC",[64,1050,71],{"class":70},[64,1052,1053],{"class":78},"TabsProps",[64,1055,1056],{"class":70},"> {\n",[64,1058,1059,1062,1064,1067,1069,1071,1074,1076,1078,1080,1082,1085],{"class":66,"line":202},[64,1060,1061],{"class":373},"  List",[64,1063,831],{"class":82},[64,1065,1066],{"class":78},"  React",[64,1068,244],{"class":70},[64,1070,1048],{"class":78},[64,1072,1073],{"class":70},"\u003C{ ",[64,1075,379],{"class":373},[64,1077,831],{"class":82},[64,1079,1043],{"class":78},[64,1081,244],{"class":70},[64,1083,1084],{"class":78},"ReactNode",[64,1086,1087],{"class":70}," }>;\n",[64,1089,1090,1093,1095,1098,1100,1102,1104,1106,1108,1110,1113,1115,1117,1119,1121,1123],{"class":66,"line":226},[64,1091,1092],{"class":373},"  Tab",[64,1094,831],{"class":82},[64,1096,1097],{"class":78},"   React",[64,1099,244],{"class":70},[64,1101,1048],{"class":78},[64,1103,1073],{"class":70},[64,1105,486],{"class":373},[64,1107,831],{"class":82},[64,1109,994],{"class":74},[64,1111,1112],{"class":70},"; ",[64,1114,379],{"class":373},[64,1116,831],{"class":82},[64,1118,1043],{"class":78},[64,1120,244],{"class":70},[64,1122,1084],{"class":78},[64,1124,1087],{"class":70},[64,1126,1127,1130,1132,1134,1136,1138,1140,1142,1144,1146,1148,1150,1152,1154,1156,1158],{"class":66,"line":362},[64,1128,1129],{"class":373},"  Panel",[64,1131,831],{"class":82},[64,1133,1043],{"class":78},[64,1135,244],{"class":70},[64,1137,1048],{"class":78},[64,1139,1073],{"class":70},[64,1141,486],{"class":373},[64,1143,831],{"class":82},[64,1145,994],{"class":74},[64,1147,1112],{"class":70},[64,1149,379],{"class":373},[64,1151,831],{"class":82},[64,1153,1043],{"class":78},[64,1155,244],{"class":70},[64,1157,1084],{"class":78},[64,1159,1087],{"class":70},[64,1161,1162],{"class":66,"line":385},[64,1163,355],{"class":70},[64,1165,1166],{"class":66,"line":412},[64,1167,285],{"emptyLinePlaceholder":284},[64,1169,1170,1172,1174,1176,1178,1180,1182,1185,1187,1190,1193,1196,1198,1201,1203],{"class":66,"line":420},[64,1171,261],{"class":82},[64,1173,367],{"class":78},[64,1175,267],{"class":82},[64,1177,478],{"class":82},[64,1179,367],{"class":78},[64,1181,273],{"class":70},[64,1183,1184],{"class":373},"props",[64,1186,831],{"class":82},[64,1188,1189],{"class":78}," TabsProps",[64,1191,1192],{"class":70},") { ",[64,1194,1195],{"class":698},"\u002F* ... *\u002F",[64,1197,509],{"class":70},[64,1199,1200],{"class":82},"as",[64,1202,1037],{"class":78},[64,1204,811],{"class":70},[64,1206,1207,1209,1211],{"class":66,"line":435},[64,1208,655],{"class":70},[64,1210,83],{"class":82},[64,1212,660],{"class":70},[64,1214,1215,1217,1219],{"class":66,"line":441},[64,1216,665],{"class":70},[64,1218,83],{"class":82},[64,1220,670],{"class":70},[64,1222,1223,1225,1227],{"class":66,"line":451},[64,1224,675],{"class":70},[64,1226,83],{"class":82},[64,1228,680],{"class":70},[15,1230,48,1231,1234],{},[19,1232,1233],{},"as TabsComponent"," cast is necessary because TypeScript can't infer static properties from assignment.",[10,1236,1238],{"id":1237},"performance-context-re-render-trap","Performance: Context Re-render Trap",[15,1240,1241,1242,1245],{},"Every consumer of a Context re-renders when the ",[50,1243,1244],{},"context value reference changes",". If you create a new object literal on every render, all sub-components re-render even when the active tab didn't change.",[55,1247,1249],{"className":57,"code":1248,"language":59,"meta":60,"style":60},"\u002F\u002F Problem: new object reference each render\n\u003CTabsCtx.Provider value={{ active, setActive }}>\n\n\u002F\u002F Fix 1: stabilise with useMemo\nconst ctx = useMemo(() => ({ active, setActive }), [active]);\n\u003CTabsCtx.Provider value={ctx}>\n\n\u002F\u002F Fix 2: split into state + dispatch contexts\nconst TabsStateCtx    = createContext(null); \u002F\u002F rerenders on active change\nconst TabsDispatchCtx = createContext(null); \u002F\u002F stable — setActive never changes\n",[19,1250,1251,1256,1269,1273,1278,1283,1296,1300,1305,1310],{"__ignoreMap":60},[64,1252,1253],{"class":66,"line":67},[64,1254,1255],{"class":698},"\u002F\u002F Problem: new object reference each render\n",[64,1257,1258,1260,1263,1265,1267],{"class":66,"line":93},[64,1259,71],{"class":70},[64,1261,1262],{"class":74},"TabsCtx.Provider",[64,1264,113],{"class":78},[64,1266,83],{"class":82},[64,1268,432],{"class":70},[64,1270,1271],{"class":66,"line":104},[64,1272,285],{"emptyLinePlaceholder":284},[64,1274,1275],{"class":66,"line":144},[64,1276,1277],{"class":70},"\u002F\u002F Fix 1: stabilise with useMemo\n",[64,1279,1280],{"class":66,"line":165},[64,1281,1282],{"class":70},"const ctx = useMemo(() => ({ active, setActive }), [active]);\n",[64,1284,1285,1287,1289,1291,1293],{"class":66,"line":175},[64,1286,71],{"class":70},[64,1288,1262],{"class":74},[64,1290,113],{"class":78},[64,1292,83],{"class":82},[64,1294,1295],{"class":70},"{ctx}>\n",[64,1297,1298],{"class":66,"line":202},[64,1299,285],{"emptyLinePlaceholder":284},[64,1301,1302],{"class":66,"line":226},[64,1303,1304],{"class":70},"\u002F\u002F Fix 2: split into state + dispatch contexts\n",[64,1306,1307],{"class":66,"line":362},[64,1308,1309],{"class":70},"const TabsStateCtx    = createContext(null); \u002F\u002F rerenders on active change\n",[64,1311,1312],{"class":66,"line":385},[64,1313,1314],{"class":70},"const TabsDispatchCtx = createContext(null); \u002F\u002F stable — setActive never changes\n",[15,1316,1317,1318,1320],{},"Splitting into state and dispatch contexts is the most effective solution: components that only call ",[19,1319,398],{}," subscribe to the dispatch context and never re-render when the active value changes. Start with a single context; split only when profiling confirms unnecessary re-renders.",[10,1322,1324],{"id":1323},"react-server-components-caveat","React Server Components Caveat",[15,1326,1327,1328,1331,1332,1335],{},"Context is a client-only API. Compound components that use it must be marked ",[19,1329,1330],{},"'use client'",", making the compound root a client boundary. You can still pass ",[50,1333,1334],{},"server-rendered children"," into a client compound component — RSC allows server components to appear as children of client components:",[55,1337,1339],{"className":969,"code":1338,"language":971,"meta":60,"style":60},"'use client'; \u002F\u002F compound component must be a client component\n\u002F\u002F tabs.tsx — full compound implementation\n\n\u002F\u002F page.tsx (server component)\nimport Tabs from '.\u002Ftabs';\n\nexport default function Page() {\n  return (\n    \u003CTabs defaultValue=\"overview\">\n      \u003CTabs.Panel value=\"overview\">\n        \u003CServerDataTable \u002F> {\u002F* server component passed as children *\u002F}\n      \u003C\u002FTabs.Panel>\n    \u003C\u002FTabs>\n  );\n}\n",[19,1340,1341,1350,1355,1359,1364,1380,1384,1397,1403,1417,1432,1448,1457,1465,1469],{"__ignoreMap":60},[64,1342,1343,1345,1347],{"class":66,"line":67},[64,1344,1330],{"class":86},[64,1346,1112],{"class":70},[64,1348,1349],{"class":698},"\u002F\u002F compound component must be a client component\n",[64,1351,1352],{"class":66,"line":93},[64,1353,1354],{"class":698},"\u002F\u002F tabs.tsx — full compound implementation\n",[64,1356,1357],{"class":66,"line":104},[64,1358,285],{"emptyLinePlaceholder":284},[64,1360,1361],{"class":66,"line":144},[64,1362,1363],{"class":698},"\u002F\u002F page.tsx (server component)\n",[64,1365,1366,1369,1372,1375,1378],{"class":66,"line":165},[64,1367,1368],{"class":82},"import",[64,1370,1371],{"class":70}," Tabs ",[64,1373,1374],{"class":82},"from",[64,1376,1377],{"class":86}," '.\u002Ftabs'",[64,1379,811],{"class":70},[64,1381,1382],{"class":66,"line":175},[64,1383,285],{"emptyLinePlaceholder":284},[64,1385,1386,1388,1390,1392,1395],{"class":66,"line":202},[64,1387,689],{"class":82},[64,1389,692],{"class":82},[64,1391,478],{"class":82},[64,1393,1394],{"class":78}," Page",[64,1396,296],{"class":70},[64,1398,1399,1401],{"class":66,"line":226},[64,1400,347],{"class":82},[64,1402,417],{"class":70},[64,1404,1405,1407,1409,1411,1413,1415],{"class":66,"line":362},[64,1406,107],{"class":70},[64,1408,75],{"class":74},[64,1410,79],{"class":78},[64,1412,83],{"class":82},[64,1414,87],{"class":86},[64,1416,90],{"class":70},[64,1418,1419,1422,1424,1426,1428,1430],{"class":66,"line":385},[64,1420,1421],{"class":70},"      \u003C",[64,1423,180],{"class":74},[64,1425,113],{"class":78},[64,1427,83],{"class":82},[64,1429,87],{"class":86},[64,1431,90],{"class":70},[64,1433,1434,1437,1440,1443,1446],{"class":66,"line":412},[64,1435,1436],{"class":70},"        \u003C",[64,1438,1439],{"class":74},"ServerDataTable",[64,1441,1442],{"class":70}," \u002F> {",[64,1444,1445],{"class":698},"\u002F* server component passed as children *\u002F",[64,1447,355],{"class":70},[64,1449,1450,1453,1455],{"class":66,"line":420},[64,1451,1452],{"class":70},"      \u003C\u002F",[64,1454,180],{"class":74},[64,1456,90],{"class":70},[64,1458,1459,1461,1463],{"class":66,"line":435},[64,1460,444],{"class":70},[64,1462,75],{"class":74},[64,1464,90],{"class":70},[64,1466,1467],{"class":66,"line":441},[64,1468,454],{"class":70},[64,1470,1471],{"class":66,"line":451},[64,1472,355],{"class":70},[10,1474,1476],{"id":1475},"testing-strategy","Testing Strategy",[15,1478,1479,1480,1483],{},"Test compound components ",[50,1481,1482],{},"through their composed API"," — render the parent with sub-components just as a real consumer would. Never import and render sub-components in isolation; they depend on the Context being present.",[55,1485,1487],{"className":57,"code":1486,"language":59,"meta":60,"style":60},"test('switches panels on tab click', async () => {\n  render(\n    \u003CTabs defaultValue=\"a\">\n      \u003CTabs.List>\n        \u003CTabs.Tab value=\"a\">Tab A\u003C\u002FTabs.Tab>\n        \u003CTabs.Tab value=\"b\">Tab B\u003C\u002FTabs.Tab>\n      \u003C\u002FTabs.List>\n      \u003CTabs.Panel value=\"a\">Content A\u003C\u002FTabs.Panel>\n      \u003CTabs.Panel value=\"b\">Content B\u003C\u002FTabs.Panel>\n    \u003C\u002FTabs>\n  );\n\n  await userEvent.click(screen.getByRole('button', { name: 'Tab B' }));\n  expect(screen.getByText('Content B')).toBeVisible();\n});\n",[19,1488,1489,1511,1519,1534,1542,1561,1581,1589,1608,1627,1635,1639,1643,1674,1697],{"__ignoreMap":60},[64,1490,1491,1494,1496,1499,1501,1504,1507,1509],{"class":66,"line":67},[64,1492,1493],{"class":78},"test",[64,1495,273],{"class":70},[64,1497,1498],{"class":86},"'switches panels on tab click'",[64,1500,26],{"class":70},[64,1502,1503],{"class":82},"async",[64,1505,1506],{"class":70}," () ",[64,1508,574],{"class":82},[64,1510,984],{"class":70},[64,1512,1513,1516],{"class":66,"line":93},[64,1514,1515],{"class":78},"  render",[64,1517,1518],{"class":70},"(\n",[64,1520,1521,1523,1525,1527,1529,1532],{"class":66,"line":104},[64,1522,107],{"class":70},[64,1524,75],{"class":74},[64,1526,79],{"class":78},[64,1528,83],{"class":82},[64,1530,1531],{"class":86},"\"a\"",[64,1533,90],{"class":70},[64,1535,1536,1538,1540],{"class":66,"line":144},[64,1537,1421],{"class":70},[64,1539,99],{"class":74},[64,1541,90],{"class":70},[64,1543,1544,1546,1548,1550,1552,1554,1557,1559],{"class":66,"line":165},[64,1545,1436],{"class":70},[64,1547,110],{"class":74},[64,1549,113],{"class":78},[64,1551,83],{"class":82},[64,1553,1531],{"class":86},[64,1555,1556],{"class":70},">Tab A\u003C\u002F",[64,1558,110],{"class":74},[64,1560,90],{"class":70},[64,1562,1563,1565,1567,1569,1571,1574,1577,1579],{"class":66,"line":175},[64,1564,1436],{"class":70},[64,1566,110],{"class":74},[64,1568,113],{"class":78},[64,1570,83],{"class":82},[64,1572,1573],{"class":86},"\"b\"",[64,1575,1576],{"class":70},">Tab B\u003C\u002F",[64,1578,110],{"class":74},[64,1580,90],{"class":70},[64,1582,1583,1585,1587],{"class":66,"line":202},[64,1584,1452],{"class":70},[64,1586,99],{"class":74},[64,1588,90],{"class":70},[64,1590,1591,1593,1595,1597,1599,1601,1604,1606],{"class":66,"line":226},[64,1592,1421],{"class":70},[64,1594,180],{"class":74},[64,1596,113],{"class":78},[64,1598,83],{"class":82},[64,1600,1531],{"class":86},[64,1602,1603],{"class":70},">Content A\u003C\u002F",[64,1605,180],{"class":74},[64,1607,90],{"class":70},[64,1609,1610,1612,1614,1616,1618,1620,1623,1625],{"class":66,"line":362},[64,1611,1421],{"class":70},[64,1613,180],{"class":74},[64,1615,113],{"class":78},[64,1617,83],{"class":82},[64,1619,1573],{"class":86},[64,1621,1622],{"class":70},">Content B\u003C\u002F",[64,1624,180],{"class":74},[64,1626,90],{"class":70},[64,1628,1629,1631,1633],{"class":66,"line":385},[64,1630,444],{"class":70},[64,1632,75],{"class":74},[64,1634,90],{"class":70},[64,1636,1637],{"class":66,"line":412},[64,1638,454],{"class":70},[64,1640,1641],{"class":66,"line":420},[64,1642,285],{"emptyLinePlaceholder":284},[64,1644,1645,1648,1651,1654,1657,1660,1662,1665,1668,1671],{"class":66,"line":435},[64,1646,1647],{"class":82},"  await",[64,1649,1650],{"class":70}," userEvent.",[64,1652,1653],{"class":78},"click",[64,1655,1656],{"class":70},"(screen.",[64,1658,1659],{"class":78},"getByRole",[64,1661,273],{"class":70},[64,1663,1664],{"class":86},"'button'",[64,1666,1667],{"class":70},", { name: ",[64,1669,1670],{"class":86},"'Tab B'",[64,1672,1673],{"class":70}," }));\n",[64,1675,1676,1679,1681,1684,1686,1689,1692,1695],{"class":66,"line":441},[64,1677,1678],{"class":78},"  expect",[64,1680,1656],{"class":70},[64,1682,1683],{"class":78},"getByText",[64,1685,273],{"class":70},[64,1687,1688],{"class":86},"'Content B'",[64,1690,1691],{"class":70},")).",[64,1693,1694],{"class":78},"toBeVisible",[64,1696,516],{"class":70},[64,1698,1699],{"class":66,"line":451},[64,1700,1701],{"class":70},"});\n",[15,1703,1704],{},"Test behavior — what users see and can do — not implementation details like which Context value changed.",[10,1706,1708],{"id":1707},"key-takeaways","Key Takeaways",[1710,1711,1712,1720,1723,1732,1740,1756,1763,1769],"ul",{},[1713,1714,1715,1716,1719],"li",{},"Compound components solve ",[50,1717,1718],{},"prop explosion"," by splitting a monolithic component into cooperating sub-components that share state via a private Context.",[1713,1721,1722],{},"The parent provides state through Context; sub-components consume it without the caller wiring anything up.",[1713,1724,1725,1726,320,1728,1731],{},"A ",[50,1727,618],{},[19,1729,1730],{},"if (!ctx) throw",") is mandatory — it turns silent misuse into a clear error.",[1713,1733,48,1734,320,1737,1739],{},[50,1735,1736],{},"dot-notation API",[19,1738,110],{},") groups related components under one import and signals their relationship.",[1713,1741,1742,1743,320,1745,1747,1748,1750,1751,320,1753,1755],{},"Support both ",[50,1744,723],{},[19,1746,486],{},"\u002F",[19,1749,729],{},") and ",[50,1752,716],{},[19,1754,374],{},") modes.",[1713,1757,1758,1759,1762],{},"Stabilise the Context value with ",[19,1760,1761],{},"useMemo"," or split into state + dispatch contexts to avoid unnecessary re-renders.",[1713,1764,1765,1766,1768],{},"In RSC, the compound root must be ",[19,1767,1330],{},"; server-component children can still be passed in.",[1713,1770,1771],{},"Always test through the composed API; sub-components tested in isolation will fail because there is no Context provider.",[1773,1774,1775],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":60,"searchDepth":93,"depth":93,"links":1777},[1778,1779,1780,1781,1782,1783,1784,1785,1786,1787],{"id":12,"depth":93,"text":13},{"id":247,"depth":93,"text":248},{"id":636,"depth":93,"text":637},{"id":709,"depth":93,"text":710},{"id":936,"depth":93,"text":937},{"id":962,"depth":93,"text":963},{"id":1237,"depth":93,"text":1238},{"id":1323,"depth":93,"text":1324},{"id":1475,"depth":93,"text":1476},{"id":1707,"depth":93,"text":1708},"Master React compound components for interviews — Context-based state sharing, dot-notation APIs, controlled vs uncontrolled modes, TypeScript typing, and performance optimisation patterns.","medium","md","React","react",{},"\u002Fblog\u002Freact-compound-components-guide","\u002Freact\u002Fpatterns\u002Fcompound-components",{"title":5,"description":1788},"blog\u002Freact-compound-components-guide","Compound Components","Patterns","patterns","2026-06-24","RnVEl5oD_A55QUODc5U79uQzok3NWiLCW91hcF4pahs",1782244083220]