Get eBook BundleVolume I index
Components Before Abstractions

Children as Composition

Ishtmeet Singh @ishtms/July 20, 2026/12 min read
#react#children#composition#components#props

So far we've been using props for normal values. Strings, numbers, booleans, objects, stuff like that. But props can carry React content too. You've probably already written JSX like this:

jsx
<Panel>
  <p>Read the lesson before continuing.</p>
</Panel>

What happens to that <p> sitting between <Panel> and </Panel>?

React doesn't treat it as some special piece of syntax that only works there. That paragraph gets passed to Panel through a prop named children.

So this:

jsx
<Panel>
  <p>Read the lesson before continuing.</p>
</Panel>

means Panel receives the paragraph through props.children.

The parent decides what content goes inside. Panel decides where that content gets placed in its own returned JSX.

And yeah, that's pretty much the main idea behind composition here.

The wrapper doesn't need to import every component that might possibly appear inside it. You give the wrapper some React content, and it places that content wherever {children} appears.

Receiving children

Let's start with the smallest useful version:

jsx
function Panel({ children }) {
  return (
    <section className="panel">
      {children}
    </section>
  );
}

And then use it:

jsx
<Panel>
  <p>Read the lesson before continuing.</p>
</Panel>

When React creates the Panel element, its props contain the nested paragraph as children.

Conceptually, you can read it as:

text
children -> paragraph element

Then React calls Panel, and Panel puts that value inside the <section> it returns.

So the resulting browser DOM ends up with a section containing the paragraph.

One thing I want you to notice here: Panel isn't calling that paragraph or doing anything weird with HTML strings. It just received a React value through props and placed that value in its own returned JSX.

That separation is what makes wrappers useful without making them know about every piece of content they'll ever contain.

Text becomes children too

Nested content doesn't have to be another JSX element.

This works:

jsx
<Panel>Read the lesson before continuing.</Panel>

Now children is a string.

jsx
function Panel({ children }) {
  return <section>{children}</section>;
}

So when we say "children contains React content", that content can be plain text too.

It can also be numbers, React elements, components you've rendered, several nodes together, or nothing.

If you're using TypeScript, you'll very often type this kind of prop using ReactNode, because ReactNode covers the kinds of values React knows how to render.

Something along these lines:

tsx
import type { ReactNode } from "react";

function Panel({ children }: { children: ReactNode }) {
  return <section>{children}</section>;
}

Nothing too mysterious going on. We're just allowing the caller to send some renderable content into the component.

What if there are multiple children?

Okay, what happens here?

jsx
<Panel>
  <h2>Before you continue</h2>
  <p>Save the current file.</p>
</Panel>

Now we've nested two sibling elements.

You still access them through the same children prop:

jsx
function Panel({ children }) {
  return <section>{children}</section>;
}

React knows how to render all the supplied child content there.

One mistake people make after learning this is assuming children must always be a normal JavaScript array because, well, sometimes there are multiple things in it.

Don't assume that.

A single child doesn't arrive as an array, and React doesn't promise that children will always have some exact array structure you can casually poke at.

So this can be a bad idea:

jsx
children.map(child => {
  // ...
});

Because what if the caller passed only one child? What if they passed text? What if they passed nothing?

React does have the Children API for code that genuinely needs to inspect or transform child content:

jsx
import { Children } from "react";

But for normal composition, most of the time you don't need it at all. Receive children, put {children} somewhere, done.

And if your component needs actual records that you're going to map(), filter, sort, identify by IDs, and so on, then ask for an array prop directly.

jsx
<LessonList lessons={lessons} />

That's much clearer than treating arbitrary JSX children as if they were your application data.

The wrapper can still add its own markup

Using children doesn't mean the component becomes an empty shell with absolutely nothing of its own.

It can have its own structure and still leave one area open for the parent.

For example:

jsx
function Panel({ title, children }) {
  return (
    <section className="panel">
      <h2>{title}</h2>
      <div className="panel-body">
        {children}
      </div>
    </section>
  );
}

Now the parent can do this:

jsx
<Panel title="Chapter progress">
  <LessonProgress completed={2} total={6} />
</Panel>

Panel controls the section, heading, and body container. The parent chooses what actually goes inside that body.

So we've got both kinds of props here. title is normal data which Panel understands and uses for a specific job. children is content that Panel mostly doesn't care about. It just puts it in the body.

That's a very common React component API.

Fixed content or open content?

Suppose we write this:

jsx
function ProgressPanel() {
  return (
    <section>
      <h2>Progress</h2>
      <LessonProgress />
    </section>
  );
}

This component always renders LessonProgress.

And maybe that's exactly what we want. If this component specifically represents the progress section of the app, having fixed content is completely reasonable.

But now compare it with this:

jsx
function Panel({ title, children }) {
  return (
    <section>
      <h2>{title}</h2>
      {children}
    </section>
  );
}

This one doesn't know what's coming after the heading.

Could be:

jsx
<Panel title="Progress">
  <LessonProgress />
</Panel>

Or:

jsx
<Panel title="Continue reading">
  <LessonCard />
</Panel>

Or something else next month.

So don't automatically turn every component into a generic wrapper, and don't automatically hardcode everything either. Ask what the component is supposed to own.

If every ProgressPanel should contain LessonProgress, put it there.

If several sections share the same outer markup but callers need different content inside, children fits really well.

Don't make a prop for every little piece of markup

You can absolutely go too far with named props.

Imagine this card:

jsx
<Card
  heading="Props as Inputs"
  paragraph="Fourteen minutes"
  buttonText="Open"
/>

Looks fine.

Then somebody wants two paragraphs.

Okay, maybe add another prop.

Then another caller wants a list.

Then another one wants a badge before the heading.

Then somebody doesn't want a button at all.

Now Card starts collecting props for every possible variation of markup that might appear inside it, and the component gets filled with branches deciding which pieces should exist.

If the card's real job is only to provide the outer card markup, you can leave the inside open:

jsx
<Card>
  <h2>Props as Inputs</h2>
  <p>Fourteen minutes</p>
  <button type="button">Open</button>
</Card>

And Card can stay very small:

jsx
function Card({ children }) {
  return <article className="card">{children}</article>;
}

Now the caller controls the content.

But don't take this to mean named props are somehow bad. They aren't.

If Card understands a value and does something specific with it, a named prop usually makes more sense.

For example:

jsx
<Card status="completed">
  ...
</Card>

If Card has built-in behavior for "completed", "active", or "locked", then status tells you something useful about the component API.

So I usually look at it this way: if the component needs to understand the value, give it a named prop. If the component only needs a place to render caller-provided content, children is probably what you want.

children can contain your own components

There's no requirement that children contains raw HTML elements.

You can do this:

jsx
<Panel title="Lessons">
  <LessonList />
  <ContinueButton />
</Panel>

And Panel doesn't need to import either component.

That's kind of the point.

The parent already knows it wants LessonList and ContinueButton, so the parent creates those React elements and passes them inside Panel.

Panel only knows that it got some React content.

Your React component tree would still contain all of them:

text
ChapterPage
  Panel
    LessonList
    ContinueButton

But the imports don't need to follow that same nesting.

Panel can stay generic while the page decides which feature components go inside it.

This also keeps dependencies from spreading for no good reason. A generic panel doesn't suddenly need to know about lessons, buttons, progress widgets, account cards, or whatever else the app might put inside it later.

children really is just a prop

Because children is a prop, you can technically pass it the normal prop way:

jsx
<Panel children={<p>Saved</p>} />

That works.

But when you're writing JSX by hand, this is normally much easier to read:

jsx
<Panel>
  <p>Saved</p>
</Panel>

Both send the same children prop.

You'll mostly see the explicit form when props are being assembled or forwarded programmatically.

And don't do both on the same element:

jsx
<Panel children={<p>One</p>}>
  <p>Two</p>
</Panel>

Now you've written two different sources for the same prop, which is just confusing code. Pick one.

What if there are no children?

This is valid too:

jsx
<Panel title="Chapter progress" />

If nothing was nested inside Panel, then children will usually be undefined.

So with:

jsx
function Panel({ title, children }) {
  return (
    <section>
      <h2>{title}</h2>
      {children}
    </section>
  );
}

React still renders the section and heading. {children} produces nothing because there's nothing there to render.

That's fine if an empty panel is allowed by your component.

If the component really requires content, then make that requirement clear in its API. With TypeScript you can type accordingly, and at runtime you can also handle the empty case if your application needs that.

What I wouldn't do is silently invent fallback content inside Panel unless every empty panel should genuinely show that fallback.

If different callers need different empty states, let those callers decide what content to pass.

More than one content area

Most wrappers need one open area, and children handles that nicely.

Sometimes though, a layout has a few separate areas that the caller should control.

For example:

jsx
function LessonLayout({ header, sidebar, children }) {
  return (
    <div className="lesson-layout">
      <header>{header}</header>
      <aside>{sidebar}</aside>
      <main>{children}</main>
    </div>
  );
}

Now the caller can provide each one:

jsx
<LessonLayout
  header={<ChapterHeading />}
  sidebar={<LessonNav />}
>
  <LessonArticle />
</LessonLayout>

header and sidebar are completely normal props. They just happen to contain React nodes instead of strings or numbers.

Then children handles the main content because that's the natural nested area of this component.

You'll sometimes hear props like header and sidebar called named slots. React doesn't have some separate slot language feature here, though. They're just props containing React content.

And again, don't create twelve of these because maybe somebody could need them one day. If the component has that many caller-controlled sections, writing the page markup directly may be easier to understand.

Don't modify the child element you received

Let's say somebody tries this:

jsx
function Panel({ children }) {
  children.props.className = "inside-panel";

  return <section>{children}</section>;
}

Don't do this.

React elements are values describing what React should render. You shouldn't mutate their props after they've been created.

And this code has another problem anyway: it assumes children is one React element with a props object.

But we've already seen that children could be text, several nodes, undefined, and so on.

If the panel needs styling around its content, put the class on markup the panel owns:

jsx
function Panel({ children }) {
  return (
    <section className="panel-content">
      {children}
    </section>
  );
}

If a specific child needs a prop, pass that prop when creating the child:

jsx
<Panel>
  <LessonList compact />
</Panel>

Now you can look at this code and immediately see that LessonList is compact.

You don't have to inspect Panel wondering whether it secretly modifies whatever gets passed inside.

Let's use it on the chapter page

So now we can make a reusable panel:

jsx
function Panel({ title, children }) {
  return (
    <section className="panel">
      <h2>{title}</h2>
      {children}
    </section>
  );
}

And use it for progress:

jsx
<Panel title="Chapter progress">
  <LessonProgress completed={2} total={6} />
</Panel>

Then use the exact same wrapper for completely different content:

jsx
<Panel title="Continue reading">
  <LessonCard title="Conditional UI" minutes={16} />
</Panel>

Panel didn't need any change between those two calls.

It owns the common markup, while the page chooses what goes inside.

And that's really what I want you to get from children.

When you write:

jsx
<Component>
  something
</Component>

that something gets passed to the component as children.

The parent creates that content. React puts it in the children prop. The component receives it and places {children} somewhere in its returned JSX. Then React continues rendering whatever nodes were supplied there.

No HTML-string conversion, no weird parent-child magic, and the wrapper doesn't need to know every component that might be passed inside.

Once this clicks, a lot of React APIs start looking much simpler because you'll see this pattern everywhere.

Sources