Beginner25 min read

Component Composition

Master the children prop, learn composition patterns over inheritance, and build flexible, reusable components using containment and specialization patterns.

The children Prop

In HTML, elements can contain other elements: <div><p>Hello</p></div>. React works the same way. When you put JSX between the opening and closing tags of a component, React passes that JSX as a special prop called children.

jsx
function Wrapper({ children }) {
  return (
    <div style={{ border: '2px solid blue', padding: '16px' }}>
      {children}
    </div>
  );
}

// Usage:
<Wrapper>
  <h1>Hello!</h1>
  <p>This content is passed as children.</p>
</Wrapper>

The children prop can be anything: text, JSX elements, components, or even an array of elements. React renders whatever you pass as children.

jsx
// Text as children
<Wrapper>Just a string</Wrapper>

// Multiple elements as children
<Wrapper>
  <Header />
  <Sidebar />
  <MainContent />
</Wrapper>

// Nested components as children
<Wrapper>
  <Wrapper>
    <p>Nested!</p>
  </Wrapper>
</Wrapper>

The children prop is the foundation of component composition in React. It allows you to create generic container components that do not need to know their contents in advance. The component defines the structure and styling, and the caller decides what goes inside.

You can access children through destructuring ({ children }) or through props.children:

jsx
// Both are equivalent:
function Box({ children }) {
  return <div className="box">{children}</div>;
}

function Box(props) {
  return <div className="box">{props.children}</div>;
}

Composition vs. Inheritance

In object-oriented programming, inheritance is a common way to share behavior between classes. But in React, composition is always preferred over inheritance.

The React team at Meta (with thousands of components in production) has stated that they have not found any use cases where they would recommend using inheritance over composition.

Why composition wins:

  1. Flexibility. A component that uses children can wrap any content without modification. An inherited component is locked into its parent's structure.

  2. Explicitness. Props make the data flow visible. Inheritance hides behavior in the class hierarchy.

  3. Simplicity. Composition is just passing props and rendering children. Inheritance requires understanding the entire class chain.

Inheritance approach (not recommended):

jsx
// Imagine if components used inheritance:
class SuccessDialog extends Dialog {
  render() {
    return super.render({ color: 'green', title: 'Success!' });
  }
}
// Hard to understand, hard to customize

Composition approach (recommended):

jsx
function Dialog({ title, color, children }) {
  return (
    <div style={{ border: `2px solid ${color}`, padding: '16px' }}>
      <h2>{title}</h2>
      {children}
    </div>
  );
}

function SuccessDialog({ children }) {
  return (
    <Dialog title="Success!" color="green">
      {children}
    </Dialog>
  );
}

// Easy to understand, easy to customize
<SuccessDialog>
  <p>Your changes have been saved.</p>
</SuccessDialog>

Composition lets you build complex UIs from simple, reusable pieces — like LEGO blocks. Each component handles its own responsibility, and they snap together through props and children.

Specialized Components

A common composition pattern is creating specialized versions of a generic component. The specialized component wraps the generic one and provides specific default props.

jsx
// Generic component
function Button({ variant, size, children, onClick }) {
  const styles = {
    padding: size === 'large' ? '12px 24px' : '8px 16px',
    backgroundColor: variant === 'danger' ? 'red' : 'blue',
    color: 'white',
    border: 'none',
    borderRadius: '4px',
    cursor: 'pointer',
  };

  return (
    <button style={styles} onClick={onClick}>
      {children}
    </button>
  );
}

// Specialized components
function DangerButton({ children, onClick }) {
  return (
    <Button variant="danger" size="large" onClick={onClick}>
      {children}
    </Button>
  );
}

function PrimaryButton({ children, onClick }) {
  return (
    <Button variant="primary" size="large" onClick={onClick}>
      {children}
    </Button>
  );
}

This pattern is useful for:

  • UI libraries: Creating a base component with many options and then offering pre-configured versions.
  • Design systems: Ensuring consistent styling by wrapping generic components with specific defaults.
  • Domain components: Creating business-specific components from generic UI components.
jsx
// More examples of specialization:
function ErrorAlert({ children }) {
  return <Alert type="error" icon="!">{children}</Alert>;
}

function PageLayout({ children }) {
  return <Layout sidebar={<Nav />} header={<Header />}>{children}</Layout>;
}

function AdminPage({ children }) {
  return <PageLayout><RequireAuth role="admin">{children}</RequireAuth></PageLayout>;
}

Each level adds specific behavior while delegating to the generic component underneath.

Containment Pattern and Named Slots

The containment pattern is when a component does not know its children ahead of time. It acts as a container and renders whatever children are passed to it.

jsx
function Card({ title, children }) {
  return (
    <div className="card">
      <h3 className="card-title">{title}</h3>
      <div className="card-body">{children}</div>
    </div>
  );
}

<Card title="User Profile">
  <img src="avatar.png" alt="Avatar" />
  <p>John Doe</p>
  <p>john@example.com</p>
</Card>

Sometimes you need multiple "slots" — different areas of a component that accept different content. While React does not have named slots like some other frameworks, you can achieve the same thing using regular props:

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

// Usage:
<Layout
  header={<NavBar />}
  sidebar={<SideMenu items={menuItems} />}
>
  <ArticleList articles={articles} />
</Layout>

Any prop can accept JSX elements — not just children. This gives you the flexibility to create components with multiple injection points:

jsx
function Modal({ title, footer, children }) {
  return (
    <div className="modal-overlay">
      <div className="modal">
        <div className="modal-header">{title}</div>
        <div className="modal-body">{children}</div>
        <div className="modal-footer">{footer}</div>
      </div>
    </div>
  );
}

<Modal
  title={<h2>Confirm Delete</h2>}
  footer={<><button>Cancel</button><button>Delete</button></>}
>
  <p>Are you sure you want to delete this item?</p>
</Modal>

Layout Components

A particularly powerful use of composition is layout components. These components define the visual structure of a page or section and let you fill in the content.

jsx
function SplitPane({ left, right }) {
  return (
    <div style={{ display: 'flex', gap: '16px' }}>
      <div style={{ flex: 1 }}>{left}</div>
      <div style={{ flex: 1 }}>{right}</div>
    </div>
  );
}

<SplitPane
  left={<ContactList contacts={contacts} />}
  right={<ChatWindow messages={messages} />}
/>

Layout components are reusable across your entire application. Here are some common patterns:

jsx
// A centered container
function CenterContent({ maxWidth, children }) {
  return (
    <div style={{
      maxWidth: maxWidth || '800px',
      margin: '0 auto',
      padding: '0 16px',
    }}>
      {children}
    </div>
  );
}

// A stack layout (vertical spacing)
function Stack({ gap, children }) {
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: gap || '8px' }}>
      {children}
    </div>
  );
}

// Usage:
<CenterContent maxWidth="600px">
  <Stack gap="16px">
    <h1>Welcome</h1>
    <p>This is centered and stacked.</p>
    <Button>Get Started</Button>
  </Stack>
</CenterContent>

Best practices for composition:

  1. Keep components focused. Each component should do one thing well.
  2. Prefer composition over configuration. Instead of a <Card type="user" showAvatar showBio /> with many flags, compose smaller pieces: <Card><Avatar /><Bio /></Card>.
  3. Use children for the primary content and named props for secondary slots.
  4. Do not over-abstract. Only create reusable components when you actually reuse them. Premature abstraction adds complexity without benefit.

Composition in Action

html
<div id="root"></div>
<script src="https://unpkg.com/react@19/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@19/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
  function Card({ title, children }) {
    return (
      <div style={{
        border: '1px solid #ccc',
        borderRadius: '8px',
        padding: '16px',
        marginBottom: '12px',
      }}>
        <h3 style={{ marginTop: 0 }}>{title}</h3>
        <div>{children}</div>
      </div>
    );
  }

  function Badge({ color, children }) {
    return (
      <span style={{
        backgroundColor: color,
        color: 'white',
        padding: '2px 8px',
        borderRadius: '12px',
        fontSize: '12px',
      }}>
        {children}
      </span>
    );
  }

  function App() {
    return (
      <div style={{ maxWidth: '400px', margin: '20px auto' }}>
        <Card title="React Fundamentals">
          <p>Learn the core concepts of React.</p>
          <Badge color="green">Beginner</Badge>
        </Card>
        <Card title="Advanced Patterns">
          <p>Composition, render props, and more.</p>
          <Badge color="orange">Intermediate</Badge>
        </Card>
      </div>
    );
  }

  ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>

What is the 'children' prop in React?

Ready to practice?

Create your free account to access the interactive code editor, run challenges, and track your progress.