Composition over inheritance
React has no component inheritance — you specialize a base component by rendering it with preset props and children, not by extending it. Ask "does X contain/use Y", and watch for wrapper chains where a single variant prop would do.
If you came to React from a classical OOP background, your instinct for “a PrimaryButton is a kind of Button” is to extend a base class and override a method. That instinct is wrong here — and React gives you no API to act on it. There is no extends Button, no super.render(), no protected method to override. The reuse mechanism the framework hands you is different.
You specialize a component the same way you’d compose plumbing: you don’t subclass a pipe, you connect pieces. A PrimaryButton doesn’t inherit from Button — it renders a Button with some props already filled in. Once that clicks, a whole category of “how do I make a variant?” questions collapses into one move: wrap and preset.
After this lesson you can specialize a base component by composing it — rendering it with preset props and forwarding children — instead of reaching for inheritance; you can apply the “does X contain/use Y?” test to decide structure; you know why composition keeps coupling low; and you can spot the failure mode where composition is over-applied into a chain of single-purpose wrappers that one variant prop would have replaced.
React has no component inheritance — the reuse primitive is composition, not subclassing. There is no way to extend a component and override its render. What React gives you instead: a component can render another component, pass it props, and pass it children. “Specialization” therefore means “a component that renders the base with some choices already made.” This isn’t a limitation you work around; it’s the model.
type ButtonProps = React.ComponentProps<"button"> & {
variant?: "primary" | "secondary" | "ghost";
};
function Button({ variant = "secondary", className = "", ...rest }: ButtonProps) {
return <button className={`btn btn--${variant} ${className}`} {...rest} />;
}Button is the base. Everything specialized below will render this, never extend it.
Specialize by presetting props and forwarding the rest — this is the whole pattern. A PrimaryButton is just Button with variant already chosen. You forward children and remaining props so the specialized component stays a drop-in for the base.
function PrimaryButton(props: ButtonProps) {
// preset one prop, forward everything else (children, onClick, type, ...)
return <Button variant="primary" {...props} />;
}
// usage is identical to a plain Button — children flow through untouched
<PrimaryButton onClick={save}>Save changes</PrimaryButton>;Note what {...props} buys you: every prop Button accepts, PrimaryButton accepts for free, with no list to maintain. If Button gains a disabled or aria-label prop tomorrow, PrimaryButton already forwards it. That is composition doing the work inheritance pretends to do — minus the fragile base-class coupling.
children is the composition slot — it’s how the content of the base is supplied from outside. An IconButton doesn’t override how Button renders; it renders a Button whose children it has arranged. The base never knows or cares what content it received.
function IconButton({ icon, children, ...rest }: ButtonProps & { icon: React.ReactNode }) {
return (
<Button {...rest}>
<span className="btn__icon" aria-hidden>{icon}</span>
{children}
</Button>
);
}
<IconButton icon={<TrashIcon />} variant="ghost" onClick={remove}>Delete</IconButton>;IconButton composes into Button’s children slot. Button rendered {children} (via the spread ...rest reaching the underlying <button>); IconButton decided what those children are. No base method was overridden — content was passed in. “Pass content in through children” replaces “override the render method.”
The senior mental test: ask “does X use/contain Y?” (composition) before “is X a Y?” (inheritance). Phrasing the relationship as contains/uses almost always yields the React answer, and it keeps coupling low — the specialized component depends only on Button’s public props, not its internals, so it can’t break when Button’s implementation changes. Inheritance couples a subclass to a parent’s protected surface; composition couples a wrapper only to a public prop contract.
// "is-a" thinking → wants to extend (no React API for this, and it would couple tightly)
// "uses-a" thinking → renders Button with presets (the React answer, loose coupling)
function DangerButton(props: ButtonProps) {
return <Button variant="ghost" className="btn--danger" {...props} />;
}Composition is almost always the right answer in React precisely because the dependency is so thin: a wrapper that only reads Button’s props is far easier to reason about and change than one welded to its insides.
One feature, two mindsets — refactor an inheritance reflex into composition. A designer asks for three button flavors: primary, icon, and a “loading” state that shows a spinner and disables the button.
The OOP reflex (which React can’t even express, shown as pseudo-code to name the temptation):
// ✗ not valid React — there is no class to extend and no render to override
class LoadingButton extends Button {
render() {
return super.render({ disabled: this.props.loading, children: <Spinner /> });
}
}The composition version — each variant renders the base with choices made, content passed via children:
function LoadingButton({ loading, children, ...rest }: ButtonProps & { loading?: boolean }) {
return (
<Button disabled={loading} aria-busy={loading} {...rest}>
{loading ? <Spinner /> : children}
</Button>
);
}
// all three are drop-in Buttons, composed not extended:
<PrimaryButton onClick={save}>Save</PrimaryButton>
<IconButton icon={<TrashIcon />}>Delete</IconButton>
<LoadingButton loading={isSaving} onClick={save}>Save</LoadingButton>Each specialized component depends only on Button’s public props (disabled, aria-busy, children, the rest spread). None reaches into Button’s internals, so a change to how Button paints itself can’t break them. That loose coupling — guaranteed by the fact that a wrapper can only talk to props — is the structural payoff composition buys over inheritance, and it’s why composition is almost always the React answer.
▸Why this works
Why does React deliberately have no inheritance? Because a component’s job is to return UI from props, and that contract composes cleanly: a component that returns a <Button> is itself just another component returning UI. Inheritance would let a subclass reach into a base’s render internals and protected state, recreating the tight coupling and fragile-base-class problems OOP UI frameworks struggled with for decades. By offering only props and children, React forces every reuse relationship to go through a public contract — which is exactly the property that keeps large component trees changeable.
▸Common mistake
The failure mode is composition turned into its own indirection: a wrapper chain — Button → FancyButton → SuperFancyButton → MegaButton — where each layer presets one more prop and adds nothing else. Now the call site is four files away from the actual <button>, every prop has to thread through four spreads, and reading the code means opening four modules to learn that “Mega” just means variant="primary" plus a bigger padding class. When the only difference between layers is a value, that value belongs in one prop on the base (<Button variant="primary" size="lg">), not in a new component per combination. The honest count: if a wrapper exists only to preset props and is used in one place, it’s probably a variant prop wearing a component costume. Compose to vary structure or content; use a prop to vary a value.
You need a 'primary' and a 'secondary' button. A teammate proposes Button → PrimaryButton → LargePrimaryButton → CtaButton, each a one-line wrapper presetting one more prop, each used in a single place. What's the senior read?
React has no component inheritance — you can’t extend a component or override its render. You specialize by composition: render the base component with preset props and forward children and the rest via {...props}, so the specialized component stays a drop-in for the base and inherits its full prop surface for free. PrimaryButton renders <Button variant="primary" {...props} />; IconButton arranges Button’s children; LoadingButton presets disabled and swaps content. The senior test is “does X use/contain Y?” (composition) rather than “is X a Y?” (inheritance) — phrasing it that way yields the React answer and keeps coupling low, because a wrapper can only depend on the base’s public props, never its internals. But composition has a failure mode: wrapper chains (Button → FancyButton → SuperFancyButton) where each layer just presets a value. When the only difference is a value, it belongs in one variant/size prop on the base, not a component per combination. Compose to vary structure or content; use a prop to vary a value.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.