Skip to content
Laviyx Learn

Try

React course menu

Component Composition in React: children and Slots

Build flexible React components with composition: the children prop, layout components, passing components and elements as props (slots), specialisation instead of configuration, render props, and avoiding prop drilling with composition.

Intermediate9 min readUpdated
On this page

React components are combined by composition: putting components inside other components and passing pieces of UI around as props. React does not use class inheritance for sharing UI; composition covers every case more simply.

children: components as containers#

A component that renders {children} can wrap anything:

JSX
function Modal({ title, children }) {
  return (
    <div style={{ border: "1px solid #ccc", borderRadius: 10, maxWidth: 320, boxShadow: "0 4px 14px rgba(0,0,0,.12)" }}>
      <div style={{ padding: "8px 12px", borderBottom: "1px solid #eee", fontWeight: "bold" }}>{title}</div>
      <div style={{ padding: 12 }}>{children}</div>
    </div>
  );
}

export default function App() {
  return (
    <Modal title="Delete file?">
      <p style={{ marginTop: 0 }}>“report.pdf” will be permanently deleted.</p>
      <button>Cancel</button> <button style={{ color: "white", background: "crimson", border: 0, padding: "4px 10px" }}>Delete</button>
    </Modal>
  );
}
Result

Modal knows nothing about its contents: the same component works for confirmations, forms or images.

Several slots#

When a component has several places to fill, pass each as its own prop. JSX can be passed like any other value:

JSX
function PageLayout({ header, sidebar, children }) {
  return (
    <div style={{ display: "grid", gridTemplateColumns: "110px 1fr", gap: 8, fontSize: 14 }}>
      <div style={{ gridColumn: "1 / -1", background: "#e7f5ff", padding: 6 }}>{header}</div>
      <aside style={{ background: "#f8f9fa", padding: 6 }}>{sidebar}</aside>
      <main style={{ padding: 6 }}>{children}</main>
    </div>
  );
}

export default function App() {
  return (
    <PageLayout
      header={<strong>Laviyx Learn</strong>}
      sidebar={<ul style={{ margin: 0, paddingLeft: 16 }}><li>React</li><li>Python</li></ul>}
    >
      <h3 style={{ marginTop: 0 }}>Composition</h3>
      <p>The layout decides where things go; the page decides what they are.</p>
    </PageLayout>
  );
}
Result

Specialisation instead of configuration#

A generic component plus small specialised ones is clearer than one component with many flags:

JSX
function Alert({ tone, icon, children }) {
  const colors = { info: ["#e7f5ff", "#1c7ed6"], success: ["#ebfbee", "#2f9e44"], danger: ["#fff5f5", "#e03131"] };
  const [bg, fg] = colors[tone];
  return (
    <p style={{ background: bg, color: fg, padding: "8px 12px", borderRadius: 6, margin: "6px 0" }}>
      {icon} {children}
    </p>
  );
}

const SuccessAlert = ({ children }) => <Alert tone="success" icon="✔">{children}</Alert>;
const ErrorAlert = ({ children }) => <Alert tone="danger" icon="✖">{children}</Alert>;

export default function App() {
  return (
    <div>
      <Alert tone="info" icon="ℹ">Your plan renews on 1 November.</Alert>
      <SuccessAlert>Payment received.</SuccessAlert>
      <ErrorAlert>Card declined. Try another card.</ErrorAlert>
    </div>
  );
}
Result

Composition reduces prop drilling#

If a value only travels through middle components to reach a deep child, you can often build the deep child at the top and pass it down as children, so the middle components never touch the value:

JSX
// Before: Page and Sidebar only pass `user` along
<Page user={user} />                     // → <Sidebar user={user} /> → <Avatar user={user} />

// After: build Avatar where `user` lives
<Page>
  <Sidebar>
    <Avatar user={user} />
  </Sidebar>
</Page>

Try composition before reaching for context.

Render props#

Sometimes a component needs to give data back to the content it renders. It can take a function as a prop and call it with that data. This pattern is called a render prop:

JSX
import { useState } from "react";

function Hoverable({ children }) {
  const [hovered, setHovered] = useState(false);
  return (
    <div onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)} style={{ display: "inline-block" }}>
      {children(hovered)}
    </div>
  );
}

export default function App() {
  return (
    <Hoverable>
      {(hovered) => (
        <button style={{ background: hovered ? "#4c6ef5" : "#edf2ff", color: hovered ? "white" : "#364fc7", border: 0, padding: "8px 14px" }}>
          {hovered ? "Let's go!" : "Hover me"}
        </button>
      )}
    </Hoverable>
  );
}
Result

Today, custom hooks handle most of these cases more simply (const hovered = useHover(ref)), but you will meet render props in libraries.

Common mistakes#

Practice#

Practice exercise

Create a Panel component with title and actions slots plus children for the body, and use it to show a "Profile" panel with an "Edit" button in the actions slot.

Show hint

function Panel({ title, actions, children }) renders a header row with the title on the left and {actions} on the right, then the body.

Show solution
JSX
function Panel({ title, actions, children }) {
  return (
    <section style={{ border: "1px solid #ddd", borderRadius: 8, maxWidth: 340 }}>
      <header style={{ display: "flex", justifyContent: "space-between", padding: 8, borderBottom: "1px solid #eee" }}>
        <strong>{title}</strong>
        <div>{actions}</div>
      </header>
      <div style={{ padding: 8 }}>{children}</div>
    </section>
  );
}

export default function App() {
  return (
    <Panel title="Profile" actions={<button>Edit</button>}>
      <p style={{ margin: 0 }}>Name: Tara Iyer<br />City: Kochi</p>
    </Panel>
  );
}
Result

Key takeaway#

Next: Styling React Components.

Finished this lesson?