Composition over Inheritance

Composition over inheritance is one of React’s foundational design ideas: instead of creating new components by extending existing ones the way you would in an object-oriented language, you build new components by combining smaller ones together. React deliberately provides no mechanism for one component to “inherit” from another the way a JavaScript class extends a superclass. Instead, React gives you props and, in particular, the special children prop, so that components can be nested, wrapped, and configured from the outside. Understanding composition well is essential to writing idiomatic React, because nearly every reusable piece of UI you build — layouts, cards, modals, lists, form wrappers — is assembled this way.

Overview / How it works

In object-oriented programming, inheritance lets a subclass reuse and override behavior from a parent class. It sounds convenient, but in practice it tightly couples the subclass to the internal implementation details of its parent — a problem often called the “fragile base class” problem. Change something in the parent and every subclass might silently break. React’s team looked for cases where a class hierarchy would produce simpler code than composing components, and did not find compelling ones, so React never grew an inheritance API for components.

Composition solves the same reuse problem differently: rather than a component inheriting behavior, a component receives other components as data, through props. The most important prop for this is children — whatever JSX you place between a component’s opening and closing tags is automatically collected into props.children and handed to that component to render wherever it likes. A component that renders {children} doesn’t need to know what’s inside; it only needs to know where to put it. This is called the containment pattern, and it’s how generic wrapper components like panels, cards, dialogs, and layout shells are built.

A second pattern, sometimes called specialization, is when one component is a more specific version of a more general one, built by rendering the general component with a particular set of props (and sometimes particular children). A WelcomeDialog is a specialized Dialog — it doesn’t extend Dialog, it renders it with fixed title and message props.

What actually happens under the hood

It helps to remember that JSX is just sugar over function calls. <Card><p>Hi</p></Card> compiles to something like React.createElement(Card, null, React.createElement('p', null, 'Hi')). That means the <p>Hi</p> element is constructed by the caller, before Card ever runs, and is simply passed in as props.children — a plain JavaScript object describing a React element. Card never evaluates or inspects the internals of that element; it just decides where in its own returned tree to place it. This is exactly why composition doesn’t create the tight coupling that inheritance does: the parent component only ever sees an opaque element tree, never the child’s internals, state, or implementation.

This also has a performance consequence. When a component re-renders because its own state changed, React re-runs that function and reconciles the new element tree against the previous one. If you pass children down as a prop from a grandparent instead of constructing them inside the re-rendering component, the children’s element objects can stay referentially the same across a re-render, and React’s reconciler can skip re-rendering that subtree when combined with memo. Composition, in other words, isn’t just an organizational nicety — it interacts directly with how the reconciler decides what work to redo.

Syntax

There are two shapes you’ll use constantly: the generic “containment” wrapper using children, and “slot” props for multiple distinct content areas.

function Wrapper({ children }) {
  return <div className="wrapper">{children}</div>;
}

// usage
<Wrapper>
  <SomeComponent />
</Wrapper>
  • children — whatever is nested between a component’s JSX tags; received as a normal prop, rendered with {children}.
  • named slot props — any prop, such as left or header, whose value is itself a JSX element, used when a component needs more than one distinct content area.
  • configuration props — ordinary data props (title, variant, size) used to specialize a general component into a specific one.

Examples

Example 1: Containment with the children prop

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

function ProfileCard() {
  return (
    <Card>
      <h3>Jane Doe</h3>
      <p>Frontend Engineer</p>
    </Card>
  );
}

This renders a div.card containing an h3 reading “Jane Doe” and a p reading “Frontend Engineer”. Card has no idea what content it wraps — it could just as easily wrap an image, a form, or another component. That’s the point: Card only owns the visual shell (the class name, the box), and the caller owns the content.

Example 2: Specialization by rendering a general component with fixed props

function Dialog({ title, message, children }) {
  return (
    <div className="dialog">
      <h2>{title}</h2>
      <p>{message}</p>
      {children}
    </div>
  );
}

function WelcomeDialog() {
  return (
    <Dialog
      title="Welcome"
      message="Thanks for visiting our spacecraft!"
    >
      <button onClick={() => console.log('Dismissed')}>Dismiss</button>
    </Dialog>
  );
}

WelcomeDialog renders a div.dialog with the heading “Welcome”, the message “Thanks for visiting our spacecraft!”, and a Dismiss button. Clicking the button logs Output:

Dismissed

WelcomeDialog is a specialized Dialog — it supplies fixed title and message props and its own children, without Dialog needing to know anything about welcome messages specifically.

Example 3: Multiple slots with named element props

function SplitPane({ left, right }) {
  return (
    <div className="split-pane">
      <div className="split-pane-left">{left}</div>
      <div className="split-pane-right">{right}</div>
    </div>
  );
}

function Sidebar() {
  return <nav>Sidebar links</nav>;
}

function MainContent() {
  return <main>Main content area</main>;
}

function App() {
  return <SplitPane left={<Sidebar />} right={<MainContent />} />;
}

This renders a two-column layout: a left column containing a nav with “Sidebar links”, and a right column containing a main with “Main content area”. children only gives you one slot, so when a component needs two or more independent content areas, passing JSX elements through ordinary named props — here left and right — is the standard React pattern.

How it works step by step

  • Mount: React calls App, which builds the <Sidebar /> and <MainContent /> elements and passes them as the left and right props to SplitPane. React then calls SplitPane, which returns a tree referencing those already-built elements. React walks the whole resulting tree, calling every component function it finds (including Sidebar and MainContent), and commits the real DOM nodes in one pass.
  • State update: If App re-renders (say, its own state changed) but the JSX for <Sidebar /> is written identically, React creates a new element object each render — elements are cheap, plain objects — and diffs it against the previous one by type and props. Because Sidebar‘s type and props haven’t changed, React reuses the existing component instance and skips re-mounting the DOM node, only re-rendering if something inside actually differs.
  • Unmount: If SplitPane is removed from the tree (for example, a parent stops rendering it), React unmounts the whole composed subtree — SplitPane, Sidebar, and MainContent together — running any effect cleanup functions from the innermost components outward before removing the DOM nodes.

Common Mistakes

Mistake 1: Reaching for class inheritance instead of composition

Developers coming from object-oriented UI frameworks sometimes try to “extend” a component to add behavior:

class SpecialButton extends Button {
  render() {
    return super.render();
  }
}

This doesn’t work the way it would in a typical OOP framework: React function components aren’t classes you can meaningfully subclass, and even React’s older class components were never designed to be extended this way — every official pattern instead composes. The fix is to wrap or configure, not extend:

function Button({ variant = 'default', children, ...props }) {
  return (
    <button className={`btn btn-${variant}`} {...props}>
      {children}
    </button>
  );
}

function SpecialButton(props) {
  return <Button variant="special" {...props} />;
}

SpecialButton renders Button with a fixed variant prop, forwarding everything else — no inheritance required, and Button‘s internals stay fully encapsulated.

Mistake 2: Copy-pasting near-identical components instead of composing

Wrong: two components duplicated except for one heading, doubling future maintenance work.

function ErrorPanel() {
  return (
    <div className="panel">
      <h3>Error</h3>
      <p>Something went wrong.</p>
    </div>
  );
}

function WarningPanel() {
  return (
    <div className="panel">
      <h3>Warning</h3>
      <p>Please check your input.</p>
    </div>
  );
}

Corrected: one composable component, specialized by props.

function Panel({ heading, children }) {
  return (
    <div className="panel">
      <h3>{heading}</h3>
      {children}
    </div>
  );
}

function ErrorPanel() {
  return <Panel heading="Error"><p>Something went wrong.</p></Panel>;
}

function WarningPanel() {
  return <Panel heading="Warning"><p>Please check your input.</p></Panel>;
}

Now the shared shell — the outer div.panel and the h3 — lives in exactly one place, and each specific panel just supplies what’s different.

Best Practices

  • Reach for the children prop whenever a component just needs to wrap arbitrary content — layout shells, cards, modals, list containers.
  • Use named element props (left, header, footer) when a component needs more than one independent content area.
  • Specialize general components with plain data props (title, variant, size) rather than duplicating the component.
  • Never try to extend a function component with class inheritance — there is no supported mechanism for it in React.
  • Share behavior (not markup) across components with custom hooks, not base classes.
  • Compose components directly in JSX instead of drilling many props down several levels of unrelated intermediate components.
  • Keep each component focused on one job — a shell, a slot layout, or a specific configuration — and let composition combine them.
  • Use <>...</> (a Fragment) when composing would otherwise force an unnecessary wrapper div.

Practice Exercises

  • Build a Modal component that accepts children and renders them inside a centered box with a semi-transparent overlay behind it. Then build a ConfirmModal that renders Modal with a fixed message and two buttons passed as children.
  • Build a PageLayout component with three named slot props — header, sidebar, and content — and use it to assemble a page from three separate components.
  • Take two nearly identical components you’ve written before (or invent two, like SuccessBanner and ErrorBanner) and refactor them into a single composable Banner component configured with props, following the pattern from Mistake 2 above.

Summary

  • React has no component inheritance mechanism by design — composition is the recommended way to reuse UI.
  • The children prop implements the containment pattern: a wrapper renders whatever elements it’s given without knowing their internals.
  • Named element props implement multi-slot layouts when one children prop isn’t enough.
  • Specialization means rendering a general component with specific props, not subclassing it.
  • Children elements are built by the caller before the wrapper ever runs, which keeps components decoupled and lets React skip unnecessary re-renders when element references don’t change.
  • Share logic with custom hooks, share markup with composition — never with class inheritance.