EduFlow
React Fundamentals
0%

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.

  1. 1The children prop
  2. 2More than one slot
  3. 3Specialization
  4. 4Composition instead of prop drilling

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:

src/components/Panel.jsx
export default function Panel({ title, children }) {
  return (
    <section className="panel">
      <h2>{title}</h2>
      <div className="panel-body">{children}</div>
    </section>
  );
}
src/App.jsx
<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:

src/components/Panel.jsx
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>
  );
}
JSX
<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:

src/components/Button.jsx
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:

JSX
<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:

JSX
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 children prop.
  • A prop can hold JSX, so a component can expose several named slots such as actions or footer.
  • 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.

Further reading

Check your understanding

4 questions

Question 1 of 4

What renders inside the <section>?

JSX
function Panel({ title }) {
  return <section><h2>{title}</h2></section>;
}

<Panel title="To watch">
  <MovieList movies={movies} />
</Panel>
Question 2 of 4

You need a Panel with a title, a body, and optional buttons in the top-right corner. Which API is the most flexible and readable?

Question 3 of 4

App has the movies array. Layout renders Sidebar, which renders WatchCount, the only one that needs movies. What’s the composition fix to avoid passing movies through Layout and Sidebar?

Question 4 of 4

A Button component takes icon and renders <span>{icon}</span>. Which call shows a star icon?

Practice

Code playground

A Panel with slots

App already uses Panel the way it should work, but Panel ignores most of what it’s given. Update only Panel so that:

  • whatever is passed in actions appears inside the <header>, after the <h2>;
  • whatever is nested between <Panel> tags (its children) appears inside the <section>, after the <header>, not inside it.

The Watched panel has no actions, so it must render no button.

function Panel({ title }) {
  // Add an actions slot in the header and render children in the body
  return (
    <section>
      <header>
        <h2>{title}</h2>
      </header>
    </section>
  );
}

export default function App() {
  return (
    <main>
      <Panel title="To watch" actions={<button>Shuffle</button>}>
        <ul>
          <li>Past Lives</li>
          <li>Paddington 2</li>
        </ul>
      </Panel>
      <Panel title="Watched">
        <p>You've watched 1 movie.</p>
      </Panel>
    </main>
  );
}
Loading the editor…

Ctrl/⌘ + Enter to run · Runs in a sandbox in your browser. Nothing leaves your device.

Preview

Console

(no output)

Checks

  • Two panels titled “To watch” and “Watched”
  • The To watch panel shows its two list items, outside the header
  • The Watched panel shows its paragraph, outside the header
  • The Shuffle button appears inside the To watch header
  • The Watched panel has no button