ADVANCED REACTJS • LESSON 34 OF 36

Composition and Compound Components

Design flexible component APIs without duplicating markup.

Read the explanation, run the worked code, then complete the practice lab in Assignments. This advanced lesson includes worked code and a dedicated practice lab.

Understand the idea

Composition lets a component receive UI through children or named slots. A dialog can accept a title, body and actions without knowing the contents of each. Prefer a small explicit contract over a long list of boolean props that create many combinations. Controlled components receive their current value and an onChange callback; the parent owns the decision.

Compound components cooperate through shared context while letting the caller arrange their children. The example exposes Accordion, Item, Toggle and Panel. The root owns which item is open; each item gives its descendants an identifier; toggles and panels read that relationship. Native buttons provide Enter and Space activation. aria-expanded and aria-controls communicate the relationship, and useId avoids duplicate DOM IDs when several accordions exist.

An API should fail clearly when a child is used outside its required provider. Context is an implementation detail of this composition, not a reason to place all application state in one provider. Start with direct props; introduce compound structure when callers truly benefit from rearranging or customizing parts.

Caller composes parts ↓ Root owns shared state ↓ Parts access scoped context ↓ Accessible UI reflects state
Trace the values through each step before changing the code.

Run the example

Use a separate React practice project with a Node.js version supported by Vite. Create it once with these commands, then use its development URL. Do not paste this into the Edupoly website’s own source files.

npm create vite@latest react-practice -- --template react
cd react-practice
npm install
npm run dev

Replace src/App.jsx with the following complete example. Keep Vite’s generated src/main.jsx entry point.

src/App.jsx

import { createContext, useContext, useId, useState } from 'react';
const RootContext = createContext(null); const ItemContext = createContext(null);
function useItem() {
  const root = useContext(RootContext), item = useContext(ItemContext);
  if (!root || !item) throw new Error('Accordion parts require Accordion and Item');
  return {...root, ...item};
}
function Accordion({children}) {
  const [open, setOpen] = useState(null);
  return <RootContext.Provider value={{open, setOpen}}>{children}</RootContext.Provider>;
}
function Item({value, children}) {
  const id = useId(); return <ItemContext.Provider value={{value, id}}><section>{children}</section></ItemContext.Provider>;
}
function Toggle({children}) {
  const {open, setOpen, value, id} = useItem();
  return <h2><button id={`${id}-button`} aria-expanded={open === value} aria-controls={`${id}-panel`} onClick={() => setOpen(open === value ? null : value)}>{children}</button></h2>;
}
function Panel({children}) {
  const {open, value, id} = useItem();
  return <div id={`${id}-panel`} role="region" aria-labelledby={`${id}-button`} hidden={open !== value}>{children}</div>;
}
export default function App() {
  return <Accordion><Item value="notes"><Toggle>Notes</Toggle><Panel>Read the lesson.</Panel></Item><Item value="practice"><Toggle>Practice</Toggle><Panel>Build the assignment.</Panel></Item></Accordion>;
}

Expected result

Opening Practice closes Notes; activating the open section closes it. Keyboard activation works and each toggle reports its expanded state.

Apply this to your assignments

Create two independent accordions and confirm their IDs and state do not collide. Add a controlled root accepting value and onValueChange. Test malformed composition and decide what useful error to show. Apply named slots to your reusable card or gallery.

Practice starting point

Use the worked example above as your starting point.

Open Assignments for this lesson’s practice lab and acceptance checks.

Common mistakes

Use stable item values, not display text that may change. Do not replace semantic buttons with clickable divs. Avoid a global context when each widget needs independent state.

Check your understanding

  1. Predict the output before running the example, then compare it with the result panel.
  2. Explain which value each expression reads and which action changes it.
  3. Try the boundary cases listed in the practice task and record what you observe.
  4. Rebuild one matching assignment without copying, then compare behavior with the original example.

Reference

Official documentation for this topic · Vite setup requirements