Skip to the chapter
←ReactGGroundwork⚛React
React/Advanced/A11
44 / 57 written chapters
/

The cover
  1. B1Setup & JSX
  2. B2Thinking in React
  3. B3Components
  4. B4Props
  5. B5useState
  6. B6Events & conditionals
  7. B7Lists & keys
  8. B8Forms
  9. B9useEffect
  10. B10Lifting state & styling
  11. B11Guided project
  12. ★Cheat page
  1. I1useRef
  2. I2Context
  3. I3useReducer
  4. I4Custom hooks
  5. I5Effects in depth
  6. I6Routing
  7. I7State architecture
  8. I8Memoisation
  9. I9Boundaries & portals
  10. I10Data fetching
  11. I11Forms at scale
  12. I12State libraries
  13. I13Auth
  14. I14TanStack Query
  15. I15Testing
  16. I16TypeScript
  17. I17Animation
  18. I18E2E testing
  19. I19i18n
  20. I20File uploads
  21. I21Real-time UI
  22. I22shadcn/ui
  23. ★Cheat page
  1. A1Fiber
  2. A2Concurrent
  3. A3Suspense
  4. A4Server Components
  5. A5Server Actions
  6. A6use()
  7. A7Rendering strategies
  8. A8Effect timing
  9. A9External stores
  10. A10Refs in depth
  11. A11Component patterns
  12. A12Performance
  13. A13Accessibility
  14. A14Design systems
  15. A15Advanced TypeScript
  16. A16Architecture at scale
  17. A17Custom renderers
  18. A18Migrating & upgrading
  19. A19Production debugging
  20. A20Review & judgement
  21. A21State machines
  22. A22Feature flags
  23. A23Interview bank
  24. ★Cheat page

⚛A11 · Part 3 · Advanced

Advanced component patterns

The four shapes behind every component library you have imported.

3 min read8 sections5 exercises
44 of 57

In this chapter

  • Compound components
  • Headless components
  • Slots and asChild
  • Controlled and uncontrolled, in one component
  • Render props, and why hooks replaced most of them

and 3 more

about 3 min
08On this page
  1. Compound components
  2. Headless components
  3. Slots and asChild
  4. Controlled and uncontrolled, in one component
  5. Render props, and why hooks replaced most of them
  6. The element APIs you will meet in older code
  7. Higher-order components
  8. Choosing

Compound components

<Tabs defaultValue="a">
  <Tabs.List>
    <Tabs.Trigger value="a">First</Tabs.Trigger>
    <Tabs.Trigger value="b">Second</Tabs.Trigger>
  </Tabs.List>
  <Tabs.Panel value="a">...</Tabs.Panel>
</Tabs>

The parent owns the state and shares it through context; the children are layout the caller arranges. Compare with the alternative — <Tabs items={[...]} renderPanel={...} activeIndex={...} /> — which needs a new prop for every arrangement anyone ever wants.

const TabsContext = createContext(null);

function Tabs({ defaultValue, children }) {
  const [value, setValue] = useState(defaultValue);
  const ctx = useMemo(() => ({ value, setValue }), [value]);
  return <TabsContext value={ctx}>{children}</TabsContext>;
}

Tabs.Trigger = function Trigger({ value, children }) {
  const ctx = useContext(TabsContext);
  return (
    <button role="tab" aria-selected={ctx.value === value}
            onClick={() => ctx.setValue(value)}>{children}</button>
  );
};

The trade is that the children are coupled to the parent by context, so using Tabs.Trigger outside Tabs must fail with a clear error. That is what the throwing custom hook from the context chapter is for.

Headless components

function useDisclosure(initial = false) {
  const [isOpen, setOpen] = useState(initial);
  return {
    isOpen,
    open: useCallback(() => setOpen(true), []),
    close: useCallback(() => setOpen(false), []),
    triggerProps: { "aria-expanded": isOpen, onClick: () => setOpen((v) => !v) },
  };
}

Behaviour, accessibility and state with no markup at all — you render whatever you want and spread the props. This is the model behind Radix, Headless UI, TanStack Table and React Aria, and it is the answer to "a component library that never matches our design".

The hard part of a dropdown was never the markup. It is roving focus, typeahead, Escape, click-outside, aria attributes, and returning focus on close. A headless library gives you exactly that and leaves the appearance alone.

Slots and asChild

<Button asChild>
  <Link to="/settings">Settings</Link>      // renders one anchor, styled as a button
</Button>

Without this, a button-styled link is either a nested <button><a></a></button> — invalid HTML and broken for keyboards — or a duplicated set of styles. asChild merges the component's props onto the child element instead of rendering a wrapper.

Controlled and uncontrolled, in one component

function useControllable({ value, defaultValue, onChange }) {
  const [uncontrolled, setUncontrolled] = useState(defaultValue);
  const isControlled = value !== undefined;

  return [
    isControlled ? value : uncontrolled,
    (next) => {
      if (!isControlled) setUncontrolled(next);
      onChange?.(next);
    },
  ];
}

Every serious library supports both: defaultValue for the easy case, value plus onChange when the caller needs control. The rule is that a component must not switch modes during its life — going from undefined to a value is the React warning everyone has seen.

Render props, and why hooks replaced most of them

<MouseTracker render={({ x, y }) => <Dot x={x} y={y} />} />   // old
const { x, y } = useMouse();                                   // now

Hooks share logic without adding tree depth, so most render props became hooks. They survive where the parent must control when and how often the children render — a virtualised list rendering only visible rows, or a component that renders its children twice for measurement.

The element APIs you will meet in older code

Children.map(children, (child) => ...)     // safe iteration, handles one child or many
Children.count(children)
isValidElement(child)                       // is this an element or a string?
cloneElement(child, { extra: "prop" })      // copy with props merged in

Before context and hooks, a parent injected behaviour into its children by cloning them:

function RadioGroup({ value, onChange, children }) {
  return Children.map(children, (child) =>
    cloneElement(child, { checked: child.props.value === value, onChange })
  );
}

It works, and it is fragile: it only reaches direct children, so wrapping one in a <div> silently breaks it, and the props a child receives are invisible at the call site. Context does the same job through any depth, which is why compound components replaced this pattern.

Learn these to read older code and a few libraries, not to write new components with.

Higher-order components

const Enhanced = withAuth(withTheme(withRouter(MyComponent)));   // where does a prop come from?

Largely historical. They obscure prop origins, stack up in DevTools, and require care with displayName and static hoisting. You will meet them in older codebases and in a few libraries; you should not write new ones.

Choosing

You wantPattern
Callers to arrange the piecesCompound components
Behaviour without imposing markupHeadless hook
Styling applied to somebody else's elementasChild / slot
Both easy defaults and full controlControllable state
To control when children renderRender prop
To share logicA custom hook, every time
Interview answer, one sentence

"Compound components move layout back to the caller, headless hooks give you the hard part — focus, keyboard, aria — without imposing markup, and asChild avoids the invalid nesting you get styling a link as a button. Render props survive only where the parent must control when children render; everything else about sharing logic is a hook now."

05Practice this chapter

Opens in the editor: write it, run it, and check it against real tests.

  • 8 testsA compound Accordionadvanced
  • 10 testsA switch that is controlled or uncontrolledadvanced
  • 8 testsA polymorphic Box, and a Button built on itadvanced
  • 6 testsA higher-order component that adds one behaviour to any componentadvanced
  • 4 testsA render prop: the parent decides what to showadvanced
All 54 for this level →
A11

Finished reading?

0 of 57 chapters read in this series

→Next stop

  1. A10Previous · A10Refs in depth
  2. A11you are here
  3. Next · A12Performance workMeasure, find the actual cost, fix the structure. Memoisation is the last step, not the first.4 min read

01On this page

8 sections · 0%

  1. 01Compound components
  2. 02Headless components
  3. 03Slots and asChild
  4. 04Controlled and uncontrolled, in one component
  5. 05Render props, and why hooks replaced most of them
  6. 06The element APIs you will meet in older code
  7. 07Higher-order components
  8. 08Choosing

[] chapters · t top