Skip to content

Children and composition

HTML has always worked by nesting: a <select> wraps its <option>s, a <ul> wraps its <li>s, a <button> wraps its label. The element in the middle is a child of the element around it, and the two work together to make the thing you see. React components can work the same way. Instead of passing everything in through named props, a component can wrap around content the way native elements do, and that single idea is what separates a component you configure from a component you compose.

This chapter builds the argument the way the course does, through a Button for a component library: first children, then prop spreading, then the finishing touches that make a component reusable in practice.

Children as an interface

The Props chapter introduced children: content between a component's opening and closing tags arrives as a prop of that name. What it buys you for reuse is bigger than it looks.

jsx
function Button({ children }) {
  return <button>{children}</button>
}

// usage: <Button>Buy now</Button>

The component decides where the children render, and the caller decides what they are. Compare that with a text prop, which works until the caller wants anything beyond a string. With children, there's nothing stopping them from passing more than text:

jsx
// usage:
// <Button>
//   <CartIcon />
//   Buy now
// </Button>

An icon to the left of the label, with no icon prop, no iconPosition="left" option, no anticipation from the component's author at all. Want the icon on the right? Move it to the other side of the text. Every configuration prop the author would have had to invent dissolves into the caller writing the markup they meant.

Forwarding the rest of the props

A Button that renders a native <button> has a second problem: events. When a caller writes <Button onClick={...}>, that onClick is a custom prop landing on your component; nothing happens unless the component passes it through to the real <button> underneath. You could forward onClick by hand, then onDoubleClick, then style, then className, and still never cover everything a native button accepts. The spread syntax forwards them all at once:

jsx
function Button({ children, ...rest }) {
  return <button {...rest}>{children}</button>
}

Destructure the props your component actually handles, collect everything else with rest syntax, and spread the rest onto the underlying element. Every valid button attribute the caller passes, event handlers included, lands where it belongs. Props your component invents for itself (a variant, say) get destructured out so they never reach the DOM element at all.

Making room for your own API

Reusable components usually add options on top of the native element. The course's button takes a size ("sm" or "lg") and a variant ("success", "warning", "danger"), each mapping to a CSS class:

jsx
function Button({ children, size, variant, className, ...rest }) {
  const sizeClass = size ? `button-${size}` : ''
  const variantClass = variant ? `button-${variant}` : ''
  const allClasses = [sizeClass, variantClass, className].filter(Boolean).join(' ')

  return (
    <button className={allClasses} {...rest}>
      {children}
    </button>
  )
}

That merge avoids a subtle bug. If the component sets className and the caller also passes one, whichever lands last on the element wins: spread the rest after your own className and the caller's class silently overwrites yours; spread it before and yours overwrites theirs. Destructuring className out and merging the strings yourself is the fix. Libraries like clsx or classnames exist to do exactly this merge once the conditions multiply.

Spreading rest props is a contract, and it has sharp edges. The two the section already showed, spread order and leaked props, are the cheap ones, though the leak is quieter than most people expect.

What a leaked prop actually does

A lowercase string prop like variant lands on the DOM node as an invalid HTML attribute with no warning at all, which is the case that bites. React warns about a camelCase prop such as iconPosition and still renders it; it warns and renders nothing for a boolean value or an all-lowercase handler name such as onclick. A camelCase typo like onClik gets neither the attribute nor the warning.

ref cuts the other way: in React 19 it's an ordinary prop, so it rides along in ...rest for function components and reaches the underlying element without forwardRef.

The expensive edge is API surface. Once callers can pass arbitrary native props, they will, and every one they reach for becomes something you owe them in the next version; narrowing the passthrough later is a breaking change even though you never documented it.

The course's Avatar "mega challenge" shows the other side of composition: one component that renders three different ways, a photo when src is provided, initials when children are, an icon otherwise. Overloading like that suits a documented component library, where one flexible Avatar beats three near-identical components. In application code, reaching for it early usually signals a component doing too many jobs; two small components beat one clever one.

The pull between those two instincts, add a prop or compose from outside, runs through the rest of this section, and the next patterns, compound components and render props, are structured answers to it.

JunoLet callers put things inside A Button that renders its children doesn't need a text prop, an icon prop, and an icon-position prop. Whoever uses it writes the icon and text inside the tags in the order they want, and the Button drops that content wherever it belongs.

Nesting, the thing HTML has always done, is also React's best trick for reuse.

JunoLet callers put things inside Build reusable components on two habits: render children where the content goes, and destructure the props you handle while spreading ...rest onto the underlying element so native attributes and event handlers pass through.

Merge className yourself rather than letting spread order decide which one survives, and keep invented props like variant out of the rest object so they never hit the DOM.

JunoLet callers put things inside Treat rest-prop forwarding as part of your component's API surface: collisions resolve by spread order, un-destructured custom props leak to the DOM, and callers will depend on the passthrough.

Prefer composition through children over configuration props until a prop earns its place, and save overloaded components for documented library code where one flexible piece really does replace several.

Next up: Compound components, where one component becomes several that work together.