React

Composition instead of inheritance in components

A React component specialises through children, props and objects rather than by extending a class: props arrive per render, the class tree does not.

Key points

  • Inheritance fixes the variant at class definition; props choose it on every render.
  • A variant map replaces N subclasses with one function and one constant object.
  • Defining a component inside another creates a new type each render and remounts the subtree.

A React component specialises through children, props and objects rather than by extending a class: inheritance fixes the tree at definition, while props arrive on every render and can change.

Why extends goes nowhere

A base Button class with three subclasses covers few variants. With four independent axes (size, tone, icon, state) the combinations grow, and every new combination asks for a new file to maintain.

The same thing with one prop

const variants = {
  primary: "bg-blue-600 text-white",
  danger: "bg-red-600 text-white",
  ghost: "bg-transparent text-slate-900",
};

function Button({ variant = "primary", children, ...rest }) {
  return (
    
      {children}
    
  );
}

One function, one variant object, no hierarchy. Adding a tone means adding a key, not a class that inherits from the previous one.

Three forms of composition

  • Content props: children, header and footer decide what appears inside the structure.
  • Behaviour props: onSelect or renderItem decide what happens when the user acts.
  • Component composition: Dialog plus DialogTitle plus DialogBody build a vocabulary, not a chain of classes.

The slot as extension point

The container decides the layout, the caller decides the content: this is the same split inheritance tries to reach with protected methods, without the class constraint.

function Card({ header, children, footer }) {
  return (
    
      {header && {header}}
      {children}
      {footer && {footer}}
    
  );
}

Whoever uses Card extends nothing: they pass three pieces and the component places them. When a fourth region is needed, you add a named prop, not a level of inheritance.

The command

The rule that catches the most common case (a component defined inside another) comes from the React ESLint plugin; the configuration below turns it on from the command line.

npm install --save-dev eslint eslint-plugin-react
npx eslint src --rule '{"react/no-unstable-nested-components":"error"}'

Live records in the catalogue

Three records built from slots: card-stack stacks the cards it receives, accordion-05 takes one element per panel, nested-dialog nests a dialog inside another. They are records among the 111,985 components and the 1,556 blocks.

Source and licence

React and eslint-plugin-react ship under the MIT licence. The older React documentation page on composition and inheritance says the same in one line: React has a powerful composition model and does not recommend inheritance to reuse code between components.

The limitation

Composition moves the decision to the caller: a component with five slots guarantees no default layout, and callers can produce combinations no test predicted. A component defined inside a render also changes identity on every pass and remounts the subtree, which is why the ESLint rule exists.

Where to continue

The basics are in what React is; when an identical prop avoids a render is in React.memo; the comparison with Vue named slots is in named slots in Vue 3.

Frequently asked questions

Are composition and inheritance alternatives?

For React components, yes. A class can extend React.Component, but specialising it through inheritance buys nothing: props cover the variants without a new class.

When is a class still the right choice?

Only for an error boundary, because componentDidCatch has no hook equivalent. Everything else is better as a function with hooks.

What is the difference between children and a normal prop?

None in the engine: children is the prop JSX fills with the element content. Only the syntax you pass it with changes.

Is a variant map slower than a class?

No: the object is constant and the lookup is O(1). The real cost is the number of CSS classes generated, not the lookup.

How do you test a composed component?

From the caller's side: pass the content, check that it appears in the DOM, and check that empty slots leave no stray containers.