Section 2 · Components & Props
Composition Patterns
19 min3 min readCode playground
Lesson at a glance
Build flexible components with the children prop and JSX slots, specialize generic components, and pass elements down to avoid threading props through layers.
No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.
Your Watchlist will have several boxes that look alike: a “To watch” panel, a “Watched” panel, maybe a “Recommendations” panel later. Each has a title bar, sometimes a button or two on the right, and different content inside. You could add a prop for every possible kind of content. Or you could let the caller put whatever it wants inside, the way you put anything inside a <div>.
That second approach is composition, and it’s how most flexible React components are built.
The children prop
Anything you nest between a component’s tags arrives as a prop called children:
export default function Panel({ title, children }) {
return (
<section className="panel">
<h2>{title}</h2>
<div className="panel-body">{children}</div>
</section>
);
}<Panel title="To watch">
<MovieList movies={toWatch} />
</Panel>
<Panel title="Watched">
<p>You've watched {watched.length} movies.</p>
</Panel>Panel doesn’t know or care what’s inside. It owns the frame (the section, the heading, the styling) and the caller owns the content. You’ve used this all along without noticing: <main> and <section> take children too.
children can be anything renderable: one element, several elements, text, or nothing. Treat it as opaque and render it where it belongs. Don’t inspect it, count it or modify it; if a wrapper needs to know something about its content, that information should arrive as an ordinary prop.
The payoff shows up when the design changes. If the panels get rounded corners and a shadow next month, you edit Panel once and every panel in the app updates, whatever it contains.
More than one slot
children is one slot. When a component needs content in several places, use ordinary props that hold JSX. A panel with optional buttons in its header:
export default function Panel({ title, actions, children }) {
return (
<section className="panel">
<header className="panel-header">
<h2>{title}</h2>
{actions}
</header>
<div className="panel-body">{children}</div>
</section>
);
}<Panel
title="To watch"
actions={<button onClick={shuffle}>Shuffle</button>}
>
<MovieList movies={toWatch} />
</Panel>If actions isn’t passed, it’s undefined, which renders nothing, so the slot is optional for free. Compare this with an API like buttons={['Shuffle', 'Clear']}: Panel would need to create the buttons, wire up handlers, handle icons and disabled states, and grow a new prop for every request. With a slot, Panel decides where things go and the caller decides what they are.
Specialization
Sometimes you want a fixed variant of a generic component. Don’t copy it; render it with preset props:
export function Button({ variant = 'neutral', children, ...rest }) {
return (
<button className={`btn btn-${variant}`} {...rest}>
{children}
</button>
);
}
export function DangerButton(props) {
return <Button variant="danger" {...props} />;
}DangerButton is a Button with one decision already made. The ...rest in Button forwards everything else, such as onClick, type or disabled, to the real <button>, so callers can use it like the HTML element they already know. Fix a bug in Button and every variant gets the fix.
Composition instead of prop drilling
Here’s a problem every app hits. App owns the movies. Layout renders a Sidebar, and the sidebar shows a WatchCount. Only WatchCount needs the movies, but to get them there, you thread them through every level:
<Layout movies={movies} /> // Layout doesn't use movies…
function Layout({ movies }) {
return <Sidebar movies={movies} />; // …neither does Sidebar
}Passing data through components that only forward it is called prop drilling. It’s tedious, and every intermediate component now depends on data it doesn’t use.
Composition often removes it. Let App, which has the data, create the element, and let Layout place it:
export default function App() {
return (
<Layout sidebar={<WatchCount movies={movies} />}>
<MovieList movies={movies} />
</Layout>
);
}
function Layout({ sidebar, children }) {
return (
<div className="layout">
<aside>{sidebar}</aside>
<main>{children}</main>
</div>
);
}Layout no longer knows movies exist. It’s a pure layout component you could reuse on any page. React also offers context for data that many distant components need, such as the current theme or the signed-in user. The React docs recommend trying composition first, and it usually works.
Next, you’ll make components show different things depending on the data, from a “Watched” badge to an empty-state message, with conditional rendering.
Key takeaways
- Whatever you put between a component’s opening and closing tags arrives as its
childrenprop. - A prop can hold JSX, so a component can expose several named slots such as
actionsorfooter. - Specialize a generic component by rendering it with preset props instead of copying it.
- Passing ready-made elements down often removes the need to thread data through components that don’t use it.