JavaScript Mixins and Composition: When Inheritance is the Wrong Tool
Inheritance is the OOP feature everyone learns first and overuses for years afterward. JavaScript classes (Lesson 4.2) let you extend one parent — that's all. Single inheritance is intentional. If your design needs a class to combine behaviors from multiple sources, JavaScript wants you to use a different tool: composition, sometimes augmented with mixins.
This lesson covers what mixins are, how to build them, when composition beats inheritance entirely, and the practical patterns that mature JavaScript codebases settle on.
The problem with deep inheritance #
A classic OOP example walks you through Animal → Mammal → Dog → ServiceDog. By the time you're four levels deep, you have:
- Methods inherited from four classes
- Behavior changes that affect every descendant
- A rigid taxonomy that's hard to refactor
- A diamond problem ("my class needs methods from two unrelated parents") with no clean answer
The modern reaction: prefer composition (HAS-A) over inheritance (IS-A). A Dog HAS-A Bark behavior, HAS-A Tail behavior, HAS-A Eat behavior. Build it from components.
Composition: just hold references #
The simplest "composition" is having one class hold instances of others.
class Bark {
do(volume) { return `Woof at volume ${volume}`; }
}
class Eat {
consume(food) { return `eats ${food}`; }
}
class Dog {
constructor(name) {
this.name = name;
this.bark = new Bark();
this.eat = new Eat();
}
}
const rex = new Dog('Rex');
rex.bark.do(10); // 'Woof at volume 10'
rex.eat.consume('kibble'); // 'eats kibble'
No inheritance. Each behavior is a separate class. The Dog is the coordinator that holds them.
The advantages:
- Swap behaviors at runtime.
rex.bark = new Whisper(); - Test each behavior in isolation. No big setup chain.
- Reuse behaviors anywhere. A
Sheepcan also have anEatinstance. - No diamond problem. A
Dogcan have a hundred behaviors with no conflicts.
Mixins: attaching behavior to existing objects #
Sometimes you want methods to feel like they're on the class itself, not on a sub-object. That's what a mixin does — it adds methods to a class's prototype (or to instances) from an external source.
Mixin as plain object #
const serializable = {
toJSON() {
return JSON.stringify(this);
},
fromJSON(s) {
Object.assign(this, JSON.parse(s));
},
};
class User {
constructor(name) { this.name = name; }
}
Object.assign(User.prototype, serializable);
const u = new User('Ada');
u.toJSON(); // '{"name":"Ada"}'
The mixin's methods now appear directly on u. No inheritance involved.
Simple, easy. The downside: it's mutation. Anyone reading the file User is declared in might not realize methods are added later from another file.
Mixin as a function that takes a class #
A more discoverable pattern: the mixin is a function that wraps a class and returns a new one with extra methods.
const Serializable = (Base) => class extends Base {
toJSON() {
return JSON.stringify(this);
}
fromJSON(s) {
Object.assign(this, JSON.parse(s));
}
};
const Comparable = (Base) => class extends Base {
equals(other) {
return JSON.stringify(this) === JSON.stringify(other);
}
};
class User extends Serializable(Comparable(Object)) {
constructor(name) {
super();
this.name = name;
}
}
const u = new User('Ada');
u.toJSON(); // works
u.equals(new User('Ada')); // true
Reads like "the User class is a Serializable Comparable Object." The mixins compose by wrapping each other in the extends clause.
This pattern is used by older React mixins, Vue mixins, and a few Angular libraries. It works, but the syntax extends F(G(H)) is a bit much.
When to choose composition #
The practical default: prefer composition. Reach for mixins only when you need methods to feel native to the class.
If you can answer "yes" to any of these, you probably want composition over inheritance:
- The relationship is HAS-A, not IS-A.
- The behavior might change at runtime.
- You'd otherwise need to inherit from multiple things.
- You want to test the behavior in isolation.
- Different instances should use different versions of the behavior.
If you answer "yes" to these, a mixin (or inheritance) might be appropriate:
- The behavior is conceptually part of the class's identity.
- You want callers to use
instanceofto detect it. - It's a cross-cutting concern (logging, validation, dirty tracking) that every instance should have.
A real-world example: a stateful component pattern #
Imagine you're building UI components. Some need:
- Validation — track errors per field.
- Dirty tracking — know if the form was changed.
- Persistence — save/load to localStorage.
With inheritance — the wrong way #
class Component {}
class ValidatedComponent extends Component { /* ... */ }
class DirtyTrackingValidatedComponent extends ValidatedComponent { /* ... */ }
class PersistedDirtyTrackingValidatedComponent extends DirtyTrackingValidatedComponent { /* ... */ }
// LoginForm extends PersistedDirtyTrackingValidatedComponent
Nobody wants to write that class name.
With composition — the right way #
class LoginForm {
constructor() {
this.validator = new Validator(this);
this.dirty = new DirtyTracker(this);
this.storage = new LocalStoragePersistence(this, 'login-form');
}
setField(key, value) {
this.fields[key] = value;
this.validator.validate(key, value);
this.dirty.mark();
this.storage.save();
}
}
Reads naturally. Each behavior is testable on its own. You can have other components reuse DirtyTracker and LocalStoragePersistence directly.
The class name is LoginForm. The shape of the class is composed.
Functional composition: even less ceremony #
For data-shaped objects (no instanceof needed), plain functions composing plain values is often the cleanest answer:
function withValidation(state) {
return {
...state,
errors: validate(state.fields),
};
}
function withDirty(state, originalState) {
return {
...state,
isDirty: !shallowEqual(state.fields, originalState.fields),
};
}
const formState = withDirty(withValidation(rawState), original);
No classes. No this. Pure transformations of data.
Frameworks like Redux, React (without classes), Solid, and Svelte lean heavily on this style. We covered pure functions and higher-order patterns in Module 2.
A summary #
- Inheritance is single-parent in JS. That's by design.
- Composition (HAS-A) beats inheritance (IS-A) in most real codebases.
- Hold references to behavior objects when each instance might use different ones.
- Use mixins (functions returning extended classes) when the methods should feel native to the class.
- Use functional composition for data-shaped state where
instanceofisn't needed. - Avoid deep inheritance trees. Two levels is fine; four is a smell; six is a refactor target.
What's next #
That completes Module 4. With objects (Module 3) and classes/composition (Module 4) covered, you have the data-modeling toolkit. Module 5 dives into asynchronous JavaScript — callbacks, promises, async/await, and the event loop that ties it all together.
Try it yourself #
The composition vs inheritance trade-off is easier to feel than to read about. Predict the output:
const logger = { log() { console.log('logged'); } };
class Service {}
Object.assign(Service.prototype, logger);
const s = new Service();
s.log();
console.log('log' in s);
console.log(Object.keys(s));js_sandboxOutput:loggedtrue (the in operator walks the prototype chain)[] (the instance itself has no own properties; log lives on the prototype)Mixing into the prototype gives every instance the method, but doesn’t make it look like an instance-owned property. That’s exactly what you usually want — methods on the prototype, data on the instance.
With this one pattern, you can attach cross-cutting behaviors (logging, validation, dirty tracking, persistence) to any class — without forcing the class into a deep inheritance chain. That's the daily payoff of composition over inheritance.
Up next in JavaScript
More from this topic
Enjoyed this article?
Get new JavaScript tutorials delivered. No spam — just code-first articles when they ship.


