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,headerandfooterdecide what appears inside the structure. - Behaviour props:
onSelectorrenderItemdecide what happens when the user acts. - Component composition:
DialogplusDialogTitleplusDialogBodybuild 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.