cf.completefrontend
Open lab My progress
THE REACT FIELD GUIDE

Component composition

Build flexible React components with children, named slots, and compound components like Tabs, and refactor a card that has too many boolean props.

By the end, you can
  • 01
    Pass content, not switchesUse children and named slots, so a component draws the frame and the parent fills it with any markup.
  • 02
    Build compound componentsWrite parts like Tabs, Tab, and TabPanel that share state through context and can be arranged in any order.
  • 03
    Hand control to the callerRefactor a component with a pile of boolean props into parts, and let the caller own the state or draw each item.

One card, any content inside

A shop page is full of boxes that look alike: a product card, a signed-in card, an order summary. They share a border, some padding, and rounded corners, and hold completely different things. You could write one component per box. Or you could write one Card that draws only the frame, and let each page decide what goes inside. That second way is composition: building a screen by putting components inside other components. Click the button in the result.

One Card component, two very different cardsPop out in the code editor (opens in a new tab)JSX
import { useState } from "react"; function Card({ children }) {  return <div className="card">{children}</div>;} export default function App() {  const [signedIn, setSignedIn] = useState(true);   return (    <>      <Card>        <h3>Green tea</h3>        <p>$4 · fresh and light</p>      </Card>      <Card>        <p>{signedIn ? "Signed in as Asha" : "Signed out"}</p>        <button onClick={() => setSignedIn(!signedIn)}>          {signedIn ? "Sign out" : "Sign in"}        </button>      </Card>    </>  );}
ResultLoading…

Lines 3 to 5 are the whole Card. It takes one prop, children, and puts it inside a <div className="card">. React fills children with whatever you write between <Card> and </Card>, as the Props lesson showed. Lines 12 to 15 give the first card a heading and a price. Lines 16 to 21 give the second a sentence and a button.

Press Sign out. The sentence and the button change, and Card had nothing to do with it. The content on lines 17 to 20 is written inside App, so it reads App's state, signedIn on line 8, directly. Card never sees signedIn, and never needs a prop for it. That is the first thing composition gives you: a component that wraps content doesn't have to know anything about it.

Left: two pieces of content App writes, an h3 Green tea and a paragraph Signed in as Asha. Arrows lead into a Card window that holds one space named children. Another arrow leads to a page window showing two cards: Green tea, and Signed in as Asha.written by App<h3>Green tea</h3><p>Signed in…</p>Card{children}draws the framepageGreen teaSigned in as Ashatwo filled cards
Card draws the same frame every time and leaves a space, children. App fills it with different content, so one component draws both cards on the page.
Real-life analogyA lunchbox with compartments

A lunchbox maker doesn't decide what you eat. They shape a box with a big compartment and two small ones, and you fill them. A canteen that only takes orders on a form has to print a checkbox for every dish it might ever serve, and reprint the form for every new one.

In real life: The lunchbox maker shapes the box and its compartments
In React: Card draws the frame: the border, the padding, the corners
In real life: You choose what goes into each compartment
In React: The parent writes the content between the tags, or in a slot
In real life: An order form with a checkbox for every possible dish
In React: A component with a boolean prop for every variation
In real life: A new dish means a new checkbox on every copy of the form
In React: A new need means a new prop inside the component, shared by every page
The main idea

Component composition means building components that take other elements as content, through children or props that hold JSX, instead of a growing list of props that configure every detail. The component owns the frame and the behavior; the code that uses it decides what goes inside.

Named slots: props that hold elements

children gives a component one area to fill. Many components have more than one: a card has a title at the top, content in the middle, and buttons at the bottom. Each area can be its own prop. Because JSX makes an ordinary JavaScript object, an element, any prop can hold it, not only children. A prop that holds elements for one area is often called a slot.

A Card with title and actions slotsPop out in the code editor (opens in a new tab)JSX
function Card({ title, actions, children }) {  return (    <section className="card">      <header>{title}</header>      {children}      {actions && <footer className="actions">{actions}</footer>}    </section>  );} export default function App() {  return (    <>      <Card        title={<h3>Green tea</h3>}        actions={<button>Add to cart</button>}      >        <p>$4 · fresh and light</p>      </Card>      <Card title={<h3>Your order <span className="badge">2 items</span></h3>}>        <p>Total: $9</p>      </Card>    </>  );}
ResultLoading…

Line 1 takes three props: title, actions, and children. Line 4 places title in a <header>, line 5 places the children under it, and line 6 places actions in a <footer>. The first card fills all three: its title on line 15 is an <h3> element, its actions on line 16 are a button, and its children on line 18 are the price.

The second card, on lines 20 to 22, passes no actions. Line 6 checks with &&: when actions is undefined, React draws nothing there, not even an empty footer with its border. Its title holds more than text: a heading with a badge inside. A string prop like title="Your order" couldn't hold that badge; a slot can hold any markup.

Press Edit & run and give the second card two buttons in one slot. On line 20, before the > at the end, add actions={<><button>Checkout</button><button>Keep shopping</button></>}. A Fragment, <>…</>, groups the two buttons into one value. The footer appears with both buttons, and Card didn't change.

How to read this table: each row is something a component needs to receive, and the way to pass it.

children, a named slot, or a plain prop?
When you needUseIn this lesson
One area of free contentchildrenThe overview's Card wraps whatever you write between its tags.
Two or more areas, each in a set placeNamed slots: props like title and actionsThe slots Card puts title in its header and actions in its footer.
An area that is often emptyA slot the component checks before it draws a frame around it{actions && <footer>…</footer>} skips the footer when a card has no actions.
Plain data, like a price or a nameAn ordinary propA price is a number. The component decides how to show it.

Composition vs configuration: a card with too many props

The other way to make one component serve many pages is configuration: the component draws everything itself, and props switch parts on and off. It starts small. Then the cart page wants a smaller card, the wishlist wants different buttons, the shop wants a badge, and each request adds a prop. A component that has grown a prop for every page that uses it is sometimes called a god component: it knows about everything. Here is one, with its three users. Open the ProductCard.jsx tab, and click the buttons in the result.

Before: ProductCard configured by eleven propsPop out in the code editor (opens in a new tab)JSX
import { useState } from "react";import ProductCard from "./ProductCard.jsx"; const chai = { name: "Chai", price: 5, rating: 4.5 }; export default function App() {  const [count, setCount] = useState(0);  const add = () => setCount(count + 1);  const remove = () => setCount(Math.max(count - 1, 0));   return (    <>      <p>In cart: {count}</p>      <h4>Shop page</h4>      <ProductCard product={chai} showRating showBadge badgeText="Sale" onAdd={add} />      <h4>Cart page</h4>      <ProductCard product={chai} compact showAddButton={false} showRemoveButton onRemove={remove} />      <h4>Wishlist page</h4>      <ProductCard        product={chai}        showPrice={false}        addButtonText="Move to cart"        showRemoveButton        onAdd={add}        onRemove={remove}      />    </>  );}
ResultLoading…

In ProductCard.jsx, lines 2 to 12 list the props: the product, six on/off switches (showPrice, showRating, showBadge, showAddButton, showRemoveButton, and compact), two texts, and two handlers. Lines 15 to 23 check them one by one: every && is a part that may or may not show.

In App.jsx, line 15 is the shop page, line 17 the cart page, and lines 19 to 26 the wishlist page. Read line 17: showAddButton={false} showRemoveButton. To know what that card looks like, you have to open ProductCard.jsx and run the checks in your head. Six switches can be set in 2 × 2 × 2 × 2 × 2 × 2 = 64 ways. Most of them no page uses, but each one is a card that someone could ask for, and that a change to ProductCard could break.

Now the same three cards, composed. Card.jsx holds two small parts, and each page writes the parts it shows:

After: the same cards, composed from small partsPop out in the code editor (opens in a new tab)JSX
import { useState } from "react";import { Card, CardTitle } from "./Card.jsx"; const chai = { name: "Chai", price: 5, rating: 4.5 }; export default function App() {  const [count, setCount] = useState(0);  const add = () => setCount(count + 1);  const remove = () => setCount(Math.max(count - 1, 0));   return (    <>      <p>In cart: {count}</p>      <h4>Shop page</h4>      <Card>        <CardTitle badge="Sale">{chai.name}</CardTitle>        <p>${chai.price}</p>        <p>Rated {chai.rating} of 5</p>        <button onClick={add}>Add to cart</button>      </Card>      <h4>Cart page</h4>      <Card compact>        <CardTitle>{chai.name}</CardTitle>        <p>${chai.price}</p>        <button onClick={remove}>Remove</button>      </Card>      <h4>Wishlist page</h4>      <Card>        <CardTitle>{chai.name}</CardTitle>        <button onClick={add}>Move to cart</button>        <button onClick={remove}>Remove</button>      </Card>    </>  );}
ResultLoading…

In Card.jsx, Card on lines 1 to 3 draws the frame, with one real option, compact. CardTitle on lines 5 to 12 draws the heading, with a badge slot. In App.jsx, lines 15 to 20 are the shop page: a title with a badge, the price, the rating, and a button. Lines 22 to 26 are the cart page, and lines 28 to 32 the wishlist. Each call site reads like the card it draws. The result is the same as before, text for text and button for button.

Press Edit & run and add <p>Only 3 left</p> under line 18. The shop card shows it; the other two don't change, and neither does Card.jsx. In the configured version, the same request means a showStock prop in ProductCard.jsx, a seventh switch, and 128 combinations. The playground runs four such requests through both versions:

Playground: one more feature request
Configuration: ProductCard.jsx11 props
  • product
  • showPrice
  • showRating
  • showBadge
  • badgeText
  • showAddButton
  • addButtonText
  • showRemoveButton
  • compact
  • onAdd
  • onRemove

6 switches, 64 combinations

Composition: what changesnothing yet

Each page writes the parts it shows. Press Next request.

Try it yourself
Before any request

ProductCard starts with 11 props, and 6 of them are on/off switches: 64 ways to combine them. Press Next request to see what each new page asks for.

The four requests are typical for a shop. The configured side lists ProductCard's props after each one; new ones are marked. A switch is a prop that is only on or off, and every switch doubles the combinations. The composed side shows the only code that changes. Reset starts again.

Press Next request four times. The configured card goes from 11 props to 18, and from 64 combinations to 1024. Every new prop lives in the one file every page shares. The composed side never touches Card.jsx: each request is one change, on the page that asked for it. The last request needs a QuantityStepper, a new small component; it goes inside the card on the cart page, like any other part.

Left, configuration: a ProductCard.jsx window holding six switches: showPrice, showRating, showBadge, showAddButton, showRemoveButton, compact. Under it: 6 switches, 64 combinations. Right, composition: the shop page's Card holding four parts it chose: CardTitle with a badge, the price, the rating, and an Add to cart button.ConfigurationProductCard.jsxshowPriceshowRatingshowBadgeshowAddButtonshowRemoveButtoncompact6 switches: 64 combinationsCompositionShop page<CardTitle badge><p>$5</p><p>Rated…</p><button>Add…</button>the page picks the parts
Configuration hides the choices inside ProductCard: 6 switches give 64 combinations, and every page shares them. Composition puts the choices where the card is used: the shop page lists the four parts it wants.

Configuration isn't always wrong. A button's size, with three fixed choices that must look the same everywhere, belongs in a prop. Problems start when props describe content or layout that each page wants differently. How to read this table: each row is a question, answered for both ways.

Configuration or composition
QuestionConfiguration: one card, many propsComposition: small parts
Who decides what the card shows?The card, from the switches it receives.The page, by choosing which parts to put inside.
What does a new page need?Often a new prop in ProductCard.jsx, which every page shares.Usually one change where that page uses the card.
How many ways can the options combine?Twice as many with every switch: 6 switches already give 64.There are no switches. Each page writes what it shows.
What does a call site look like?Short, but you open ProductCard.jsx to learn what each switch does.Longer, but it reads like the screen it draws.
When is it the better choice?A few options that must look the same everywhere, like a button's size.Content and layout that differ from page to page.

Compound components share state through context

Slots work when the parts are independent. Some parts aren't: a row of tabs and the panels under them must agree on which tab is open. One way is configuration again, with every tab described in one array prop:

Tabs configured by an items array (to read, not run)JSX
<Tabs
  defaultId="details"
  items={[
    { id: "details", label: "Details", content: <p>Chai: black tea…</p> },
    { id: "reviews", label: "Reviews (2)", content: <Reviews /> },
    { id: "shipping", label: "Shipping", content: <p>Ships in 2 days…</p> },
  ]}
/>

It works until a page wants a badge on one tab, a disabled tab, or a sale banner between the tabs and the panels. Each needs a new key in items, and new code in Tabs. The composed way splits the widget into compound components: a set of parts that only work together, here Tabs, TabList, Tab, and TabPanel. The page arranges them freely, and the parts share their state in secret, through context. Click the tabs, then open the Tabs.jsx tab.

Tabs built from four parts that share one valuePop out in the code editor (opens in a new tab)JSX
import { Tabs, TabList, Tab, TabPanel } from "./Tabs.jsx"; export default function App() {  return (    <Tabs defaultValue="details">      <TabList>        <Tab value="details">Details</Tab>        <Tab value="reviews">Reviews (2)</Tab>        <Tab value="shipping">Shipping</Tab>      </TabList>      <TabPanel value="details">        <p>Chai: black tea with ginger and cardamom.</p>      </TabPanel>      <TabPanel value="reviews">        <p>Warm and spicy. (Asha)</p>        <p>My morning cup. (Ravi)</p>      </TabPanel>      <TabPanel value="shipping">        <p>Ships in 2 days. Free over $20.</p>      </TabPanel>    </Tabs>  );}
ResultLoading…

In App.jsx, line 5 opens Tabs with defaultValue="details". Lines 6 to 10 are the row of tabs, and lines 11 to 20 the three panels. Each Tab and TabPanel has a value; a tab and a panel with the same value belong together.

Now Tabs.jsx, from the top:

  • Line 3 creates TabsContext. Lines 5 to 9 are Tabs: it keeps the open value in state (line 6) and provides it, with the setter and an id from useId, to everything inside (line 8). Tabs draws no markup of its own.
  • Lines 11 to 15 are useTabs, a small custom hook every part calls. Outside a Tabs, useContext returns the default, null, and line 13 throws a clear error instead of letting the part fail somewhere later.
  • Lines 21 to 34 are Tab: a button that is selected when its value matches the shared one (line 27) and sets it when clicked (line 29). Lines 36 to 48 are TabPanel: hidden unless its value is the open one (line 43).
  • role="tab", aria-selected, role="tabpanel", and the ids that link each tab to its panel tell screen readers what the parts are. A complete tabs widget also moves between tabs with the arrow keys; the Accessible React components lesson covers keyboard support.
A function card, Tabs, holding value "details". Four arrows lead down to four parts: Tab details, selected; Tab reviews, not selected; TabPanel details, shown; TabPanel reviews, hidden and faded.Tabsvalue: "details"Tab detailsselectedTab reviewsnot selectedTabPanel detailsshownTabPanel reviewshidden
Tabs keeps one value, "details", and provides it in TabsContext. Each Tab and TabPanel reads it with useTabs() and compares it with its own value, so they always agree.

Press Edit & run. Under line 20, the last </TabPanel>, add a panel: <TabPanel value="brewing"><p>Simmer for 4 minutes with milk.</p></TabPanel>. Then, under line 9, add a tab: <Tab value="brewing">Brewing</Tab>. A fourth tab works, and Tabs.jsx didn't change. You can also put any markup between the parts, like a sale notice between </TabList> and the first panel: the parts find their context at any depth.

Older code: cloneElement and Children.map

Before context was common, compound components looped over their children with Children.map and copied each one with cloneElement to add props like isSelected. React's documentation lists both as legacy APIs. They only reach direct children, so a Tab wrapped in a TabList, or in any <div>, misses the props. Context reaches every depth. Some libraries also name the parts Tabs.List and Tabs.Tab; that is only a naming choice, and the parts work the same way.

Inversion of control: let the caller decide

In every example so far, the component gave a decision back to the code that uses it: what goes inside the card, which parts the product card has, in what order the tabs and panels sit. Handing decisions to the caller is called inversion of control. The component keeps the behavior that must always be right, and the caller decides the rest. Two more decisions are often handed over: who owns the state, and how each item of a list is drawn.

Here the product page needs a Read the 2 reviews button, outside the tabs, that opens the Reviews tab. The tabs keep their open value inside Tabs, where nothing outside can reach it. So Tabs learns to be controlled, like an <input> with value and onChange (the Sharing state lesson explains controlled and uncontrolled components). Click the button and the tabs:

Tabs the page can controlPop out in the code editor (opens in a new tab)JSX
import { useState } from "react";import { Tabs, TabList, Tab, TabPanel } from "./Tabs.jsx"; export default function App() {  const [tab, setTab] = useState("details");   return (    <>      <button onClick={() => setTab("reviews")}>Read the 2 reviews</button>      <p>Open tab: {tab}</p>      <Tabs value={tab} onValueChange={setTab}>        <TabList>          <Tab value="details">Details</Tab>          <Tab value="reviews">Reviews (2)</Tab>        </TabList>        <TabPanel value="details">          <p>Chai: black tea with ginger and cardamom.</p>        </TabPanel>        <TabPanel value="reviews">          <p>Warm and spicy. (Asha)</p>          <p>My morning cup. (Ravi)</p>        </TabPanel>      </Tabs>    </>  );}
ResultLoading…

In App.jsx, line 5 keeps the open tab in App's own state. Line 9 sets it from a button outside the tabs, line 10 shows it, and line 11 passes it down as value, with setTab as onValueChange. Clicking Read the 2 reviews opens Reviews; clicking a tab updates the text on line 10.

In Tabs.jsx, only Tabs changed (lines 5 to 21). Line 7 checks whether the caller passed a value. Line 8 uses it if so, and its own state if not, so the tabs from the previous section, with only defaultValue, still work. When a tab is clicked, select on lines 11 to 14 updates its own state only when nobody controls it, and always tells the caller through onValueChange, with optional chaining in case there is none.

A button, Read the 2 reviews, with an arrow down to a function card App labelled setTab("reviews"). App holds tab. An arrow from App to a function card Tabs is labelled value={tab}. An arrow back from Tabs to App is labelled onValueChange(next).Read the 2 reviewssetTab("reviews")Apptabowns the statevalue={tab}onValueChange(next)Tabsshows the panelfor value
The page owns the state. App passes tab down as value, and Tabs calls onValueChange when a tab is clicked. Anything else in App, like the button, can change the tab too.

Press Edit & run and delete onValueChange={setTab} from line 11. Now click a tab: nothing happens. The page controls the value and never changes it, just like an <input value> without onChange. Control comes with a duty: whoever owns the state must update it.

The other decision a component can hand over is how each item is drawn. SearchList below owns the searching: the text box, the filtering, the empty message, the list and its keys. The caller passes a function that returns JSX, renderItem, and SearchList calls it once per item. Type mint, then xyz, in the box:

A search list that lets the caller draw each rowPop out in the code editor (opens in a new tab)JSX
import { useState } from "react"; const teas = [  { id: 1, name: "Green tea", price: 4 },  { id: 2, name: "Chai", price: 5 },  { id: 3, name: "Mint tea", price: 3 },]; function SearchList({ items, renderItem, empty }) {  const [query, setQuery] = useState("");  const shown = items.filter((item) =>    item.name.toLowerCase().includes(query.toLowerCase()),  );   return (    <>      <input        aria-label="Search teas"        value={query}        onChange={(e) => setQuery(e.target.value)}      />      {shown.length === 0 ? (        empty      ) : (        <ul>          {shown.map((item) => (            <li key={item.id}>{renderItem(item)}</li>          ))}        </ul>      )}    </>  );} export default function App() {  return (    <SearchList      items={teas}      renderItem={(tea) => <>{tea.name}: <strong>${tea.price}</strong></>}      empty={<p>No teas match. Try chai.</p>}    />  );}
ResultLoading…

Line 9 takes the items, renderItem, and an empty slot. Lines 11 to 13 keep only the names that contain the text. Line 23 shows the empty slot when nothing matches, and line 27 calls renderItem(item) for each row. On line 39, the caller decides that a row is a name and a price; on line 40, what the empty message says.

Press Edit & run and change line 39 to renderItem={(tea) => <button>Add {tea.name}</button>}. Every row becomes a button; SearchList didn't change. A prop that takes a function returning JSX is also called a render prop. Older code used render props to share stateful logic between components, and custom hooks do that job now. For drawing the items of a list, like here, it is still a common pattern.

A settings page built from parts

A real settings page mixes both patterns: rows with slots for their controls, and an accordion whose sections open one at a time. Click the checkbox, then open each accordion section in turn. Open the Settings.jsx tab to read the parts.

A settings page from rows and an accordionPop out in the code editor (opens in a new tab)JSX
import { useState } from "react";import {  SettingsRow,  Accordion,  AccordionItem,  AccordionTrigger,  AccordionContent,} from "./Settings.jsx"; export default function App() {  const [receipts, setReceipts] = useState(true);   return (    <>      <h3>Settings</h3>      <SettingsRow        label="Email receipts"        description={receipts ? "On: a receipt for every order." : "Off"}        control={          <input            type="checkbox"            aria-label="Email receipts"            checked={receipts}            onChange={(e) => setReceipts(e.target.checked)}          />        }      />      <SettingsRow label="Other devices" control={<button>Sign out</button>} />      <Accordion>        <AccordionItem value="data">          <AccordionTrigger>Download my data</AccordionTrigger>          <AccordionContent>            <p>We email you a file with your orders.</p>            <button>Send the file</button>          </AccordionContent>        </AccordionItem>        <AccordionItem value="delete">          <AccordionTrigger>Delete my account</AccordionTrigger>          <AccordionContent>            <p>This can't be undone.</p>            <button>Delete account</button>          </AccordionContent>        </AccordionItem>      </Accordion>    </>  );}
ResultLoading…

In Settings.jsx, SettingsRow on lines 3 to 13 has a label, an optional description, and a control slot that holds any input. In App.jsx, lines 16 to 27 put a checkbox in that slot, and line 28 a button. The row doesn't care which: a new setting with a dropdown needs no change to SettingsRow.

The accordion is a compound component with two contexts. Accordion (lines 18 to 21) keeps which item is open. Each AccordionItem (lines 23 to 29) provides its own value in a second context, so the trigger and the content inside it don't need a value prop each. AccordionTrigger (lines 31 to 40) reads both and opens or closes its item; aria-expanded on line 36 tells screen readers whether it is open. AccordionContent (lines 42 to 46) shows its children only when its item is the open one. Open Download my data, then Delete my account: the first closes, because there is only one open value.

Plain prop, slot, or compound parts?
  • A product card's price, like 5
  • The buttons under a card, which differ on every page
  • A dialog's heading, which sometimes holds a link or a badge
  • Tabs and panels that must agree on which one is open
  • The signed-in user's name in a greeting
  • FAQ questions where opening one closes the others
  • A button's size: small, medium, or large
  • The sidebar of a page layout, which each page fills differently
Try it yourself
0 of 8 correct

Sort each need: is it data for a plain prop, content for a slot, or parts that must share state?

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

  • "Only children can hold JSX." No. JSX makes an ordinary object, so any prop can hold it. title and actions on the slots Card are props like any other.
  • "More props make a component more flexible." Not past a point. Each on/off prop doubles the combinations, and every page depends on all of them. Slots and parts let each page change only what it needs.
  • "Composition means never using props for options." No. Data, like a price, and a few fixed choices, like a button's size, are good props. Composition is for content and layout that differ by page.
  • "Compound components need cloneElement." No. That older way only reaches direct children. Context reaches the parts at any depth, inside any wrapper.
  • "Content in a slot belongs to the component it's passed to." No. The parent writes it, so it reads the parent's state and props. In the overview, the button inside Card changes App's signedIn.
  • "A controlled component updates itself." No. Once the caller passes value, the caller must change it in onValueChange. Without that, the tabs stay where they are.

How to read this table: each row is a pattern from this lesson, what it hands to the caller, and where you saw it.

Which pattern hands over which decision
PatternWhat the caller decidesIn this lesson
childrenOne area of content.Card in the overview.
Named slotsSeveral areas, each in a set place.title and actions on the slots Card.
Compound componentsThe order and the content of parts that share state.Tabs, TabList, Tab, and TabPanel.
Controlled propsThe state itself, through value and onValueChange.The tabs that the Read the 2 reviews button opens.
A function that returns JSXHow each item is drawn.renderItem in SearchList.

Practice exercises

Each exercise shows its code's result under it, or when you press Edit & run. Change the code, and the result updates as you type. Try a guess first, then use the hints.

Exercise 1 · Warm-upCount the footers

How many <footer> elements does this page draw? Answer first, then press Edit & run to check.

function Card({ title, actions, children }) {
  return (
    <section className="card">
      <header>{title}</header>
      {children}
      {actions && <footer className="actions">{actions}</footer>}
    </section>
  );
}

export default function App() {
  return (
    <>
      <Card title={<h3>Green tea</h3>} actions={<button>Add</button>}>
        <p>$4</p>
      </Card>
      <Card title={<h3>Chai</h3>}>
        <p>$5</p>
      </Card>
      <Card
        title={<h3>Mint tea</h3>}
        actions={<><button>Add</button><button>Save</button></>}
      >
        <p>$3</p>
      </Card>
    </>
  );
}

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upFill the actions slot

    Give the Your order card a Checkout button in its footer, without changing Card. How many buttons does the page show?

    function Card({ title, actions, children }) {  return (    <section className="card">      <header>{title}</header>      {children}      {actions && <footer className="actions">{actions}</footer>}    </section>  );} export default function App() {  return (    <>      <Card        title={<h3>Green tea</h3>}        actions={<button>Add to cart</button>}      >        <p>$4 · fresh and light</p>      </Card>      <Card title={<h3>Your order <span className="badge">2 items</span></h3>}>        <p>Total: $9</p>      </Card>    </>  );}
    ResultLoading…

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeFind why no panel shows

      The tabs show, but no panel does, and no tab looks selected until you click one. Find the mistake and fix it.

      import { Tabs, TabList, Tab, TabPanel } from "./Tabs.jsx"; export default function App() {  return (    <Tabs defaultValue="detail">      <TabList>        <Tab value="details">Details</Tab>        <Tab value="reviews">Reviews (2)</Tab>        <Tab value="shipping">Shipping</Tab>      </TabList>      <TabPanel value="details">        <p>Chai: black tea with ginger and cardamom.</p>      </TabPanel>      <TabPanel value="reviews">        <p>Warm and spicy. (Asha)</p>        <p>My morning cup. (Ravi)</p>      </TabPanel>      <TabPanel value="shipping">        <p>Ships in 2 days. Free over $20.</p>      </TabPanel>    </Tabs>  );}
      ResultLoading…

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeAdd a part that shows the open tab

        Add a CurrentTab part to Tabs.jsx that shows which tab is open, and use it in App.jsx. After you click Shipping, what does it show?

        import { Tabs, TabList, Tab, TabPanel } from "./Tabs.jsx"; export default function App() {  return (    <Tabs defaultValue="details">      <TabList>        <Tab value="details">Details</Tab>        <Tab value="reviews">Reviews (2)</Tab>        <Tab value="shipping">Shipping</Tab>      </TabList>      <TabPanel value="details">        <p>Chai: black tea with ginger and cardamom.</p>      </TabPanel>      <TabPanel value="reviews">        <p>Warm and spicy. (Asha)</p>        <p>My morning cup. (Ravi)</p>      </TabPanel>      <TabPanel value="shipping">        <p>Ships in 2 days. Free over $20.</p>      </TabPanel>    </Tabs>  );}
        ResultLoading…

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeReplace five props with one slot

          Banner has a switch and two texts for a link, and a switch and a handler for a Close button. Replace them with one slot, and add a third banner, Chai is back in stock, with an Add to cart button. How many props does Banner take now?

          import { useState } from "react"; function Banner({ text, showLink = false, linkText, linkHref, showClose = false, onClose }) {  return (    <div className="banner">      <p>{text}</p>      {showLink && <a href={linkHref}>{linkText}</a>}      {showClose && <button onClick={onClose}>Close</button>}    </div>  );} export default function App() {  const [shipped, setShipped] = useState(true);   return (    <>      <Banner text="Free delivery over $20." showLink linkText="See details" linkHref="#delivery" />      {shipped && (        <Banner text="Your order has shipped." showClose onClose={() => setShipped(false)} />      )}    </>  );}
          ResultLoading…

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeLet the page open a tab

            The Read the reviews button does nothing, because the open tab lives inside Tabs. Make Tabs controllable and wire up the button. After you click it, what does the panel's first line say?

            import { Tabs, TabList, Tab, TabPanel } from "./Tabs.jsx"; export default function App() {  return (    <>      <button onClick={() => {}}>Read the reviews</button>      <Tabs defaultValue="details">        <TabList>          <Tab value="details">Details</Tab>          <Tab value="reviews">Reviews (2)</Tab>        </TabList>        <TabPanel value="details">          <p>Chai: black tea with ginger and cardamom.</p>        </TabPanel>        <TabPanel value="reviews">          <p>Warm and spicy. (Asha)</p>          <p>My morning cup. (Ravi)</p>        </TabPanel>      </Tabs>    </>  );}
            ResultLoading…

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each code question, find who writes the content and who holds the state, then follow it. Press Edit & run on a question's code to check your answer.

              The component composition quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does a component receive in its children prop?

                Choose an answer to see the explanation.

              2. Question 2 of 7What does this page show, from top to bottom?

                Read the code, then predictPop out in the code editor (opens in a new tab)JSX
                function Panel({ heading, children }) {
                  return (
                    <section>
                      {heading}
                      {children}
                    </section>
                  );
                }
                
                export default function App() {
                  return (
                    <Panel heading={<h3>Your order</h3>}>
                      <p>2 items</p>
                    </Panel>
                  );
                }

                Choose an answer to see the explanation.

              3. Question 3 of 7A card has 8 on/off props. A team adds a ninth. How many ways can the switches combine now?

                Choose an answer to see the explanation.

              4. Question 4 of 7What happens when this page runs with the lesson's Tabs.jsx?

                Read the code, then predictPop out in the code editor (opens in a new tab)JSX
                import { Tabs, Tab } from "./Tabs.jsx";
                
                export default function App() {
                  return (
                    <>
                      <Tab value="details">Details</Tab>
                      <Tabs defaultValue="details" />
                    </>
                  );
                }

                Choose an answer to see the explanation.

              5. Question 5 of 7How do the lesson's Tab and TabPanel know which tab is open?

                Choose an answer to see the explanation.

              6. Question 6 of 7This page uses the controlled Tabs.jsx. You click Details in the result. Which text shows under the tabs?

                Read the code, then predictPop out in the code editor (opens in a new tab)JSX
                import { Tabs, TabList, Tab, TabPanel } from "./Tabs.jsx";
                
                export default function App() {
                  return (
                    <Tabs value="reviews">
                      <TabList>
                        <Tab value="details">Details</Tab>
                        <Tab value="reviews">Reviews</Tab>
                      </TabList>
                      <TabPanel value="details">
                        <p>Black tea with ginger.</p>
                      </TabPanel>
                      <TabPanel value="reviews">
                        <p>Warm and spicy.</p>
                      </TabPanel>
                    </Tabs>
                  );
                }

                Choose an answer to see the explanation.

              7. Question 7 of 7When is a plain prop a better choice than a slot?

                Choose an answer to see the explanation.

              Key takeaways

              • Composition builds a screen by putting components inside components. A wrapper like Card draws the frame, and the parent decides the content.
              • children fills one area. Named slots, props that hold elements, fill several. A slot can be empty, and the component skips its frame.
              • Content in a slot is written by the parent, so it reads the parent's state directly. The wrapper never needs a prop for it.
              • Configuration, a prop for every variation, doubles the combinations with every switch and puts every page's needs in one shared file. Keep props for data and a few fixed choices.
              • Compound components, like Tabs, Tab, and TabPanel, share state through context, so the page can arrange them freely. A small hook that throws outside the parent gives a clear error.
              • Inversion of control hands decisions to the caller: the content, the order, the state (value and onValueChange), or how each item is drawn (renderItem).

              Remember the one-liner.
              Let components own the frame and the behavior, and let the code that uses them decide what goes inside.

              Coming next: Portals. A modal or a tooltip is composed like any other part, but its DOM has to appear somewhere else on the page; createPortal puts it there.

              CompleteFrontend Clear concepts. Working examples.