Learn how to maximize reusability in your Angular components by using composition instead of inheritance
This is a follow-up from my previous article about Component Composition with Angular where I listed 3 ways of composing Angular components:
Class Inheritance
Class Mixins
Component Composition
TLDR; my favorite way is to compose components in small units and use Inputs and Outputs to communicate between components. In order to share logic pieces between components, I like how Mixins can help us avoid some pitfalls from using class inheritance.
In this article, I want to focus more on the relationship between Class Inheritance and Class Mixins, how they differ, and some pitfalls from using Mixins for building components.
Tip: Use tools like **Bit** (Github) to increase code reuse by sharing and collaborating on Angular components across projects. Share your reusable building blocks to a collection on bit.dev, for future compositions.
Example: Shared Angular components in a Bit collection
The Pitfalls of Class Inheritance
You probably already know why using inheritance is at times very appealing: define some methods and properties once, then use them for every common subclass: wonderful!
On a superficial level, and in some cases, that is actually a good thing. Yet, there are some well-known and documented issues that come with *class inheritance. *The most important ones, from a component architect’s point of view, are the following:
fragile base class — when a change in your base class breaks the derived subclasses
it encourages early choices when designing the base class: it makes the design brittle and fragile
it breaks encapsulation
In fact, you may have heard the legendary quote from the Gang of Four book:
Favor composition over inheritance
There are several types of components where I see inheritance used pretty often:
form fields with common value accessors
route components that extend a base-route
modal, popups, etc. with common methods (show, hide, etc.)
This article focusses more on business logic rather than purely visual attributes (disabled, animated, etc.). I found that sharing logic between components to be a little bit complex and a mostly misunderstood topic, especially when the framework itself does not provide an official stance regarding the topic, as opposite for example to React.
Typescript Mixins
The concept of Mixins is pretty simple: imagine that instead of having a hierarchy of classes, you instead have a number of very small partial classes. These classes can be combined together and build larger classes with great flexibility.
The way Mixins are created with Typescript is simple: we define a function that takes as argument a class and extend the newly created class with the one passed as the argument.
First, we define the mixins pinMixin and closeMixin that define 1 method each:
function pinMixin(BaseClass) {
return class extends BaseClass {
pin() {
// implementation
}
}
}
function closeMixin(BaseClass) {
return class extends BaseClass {
close() {
// implementation
}
}
}
We create a Base class that is created by merging the mixins functions, and then we extend the implementation:
const BaseTabMixin = pinMixin(
closeMixin(class {})
);
class Tab extends BaseTabMixin {}
// Tab now can use the methods `close` and `pin`
Scenario: A Social Media Aggregator App
As an example, I want to build a prototype of a Social Media aggregator application with a feed of posts from the main social media services.
This is a particular example that I faced many years ago as a Junior developer: Babel was released, and ES6 classes were the fancy new thing until they weren’t.
Junior me, a little naively, started creating base classes, extending left and right, and it was exciting. Look how much code I was able to share thanks to them! In the beginning, it’s something you don’t immediately realize: requirements are not fully fleshed-out, and as we all know, new details emerge continuously.
We’re going to see how to build posts components for social media like Facebook, Twitter, Youtube, and Reddit: first, we’ll be using the good old Inheritance.
Afterward, we’ll be refactoring using Composition.
Building a base post component with Inheritance
Let’s proceed and build a BasePost class that shares properties and methods that the derived subclasses will share. As you may already know, social media posts are fairly similar to each other, with subtle differences: they have an author, some content (be it text, a link, or an image), and allow some actions such as liking, sharing, editing, etc.
Our base class PostComponent will have an input (Post object) and will inject a service PostService to which we delegate our actions.
The only common action shared among all the Social posts is delete and therefore we add it to base class so that all the subclasses can inherit the method.
class PostComponent {
@Input() post: Post;
constructor(protected service: PostService) {}
delete() {
this.post.delete(this.post.id);
}
}
This is the bare minimum base class we can create. Now, we can proceed and add specific actions.
We know that both Facebook and Twitter allow posts to be liked, but not Twitter nor Youtube; therefore, we create a subclass called LikeablePost:
class LikeablePost extends PostComponent {
get likes() {
return this.post.likes;
}
like() {
this.service.like(this.post.id);
}
unlike() {
this.service.unlike(this.post.id);
}
}
Both Youtube and Reddit allow posts to be upvoted and downvoted; it makes sense to create a subclass that allows performing such actions:
class VoteablePost extends PostComponent {
downvote() {
this.service.downvote(this.post.id);
}
upvote() {
this.service.upvote(this.post.id);
}
}
Facebook and Twitter also have another similarity: the concept of “sharing” as key metadata.
class ShareablePost extends LikeablePost {
get shares() {
return this.post.shares;
}
share() {
this.service.share(this.post.id);
}
}
A similarity shared among Youtube, Facebook and Reddit are that they all allow posts to be edited, unlike Twitter.
This is the first issue we encounter:
as the method is not shared by all classes, it would be a mistake to add it to the base class
we could implement the method edit for all the subclasses, but that’d be very repetitive
We proceed by implementing TwitterPostComponent
@Component({...})
class TwitterPostComponent extends ShareablePost {}
Let’s take a jump into the future, and Jack gives us horrible news: we can no longer delete tweets! Our class now needs to change, but wait: delete is defined in the base class.
if we remove the method from the base class, we will break the other classes
if we remove it only from TwitterBaseComponent we will end up breaking the Liskov substitution principle, that means TwitterBaseComponent and PostComponent should be able to be swapped without breaking anything
If it wasn’t clear enough by now, all this was a bad idea.
Enter Composition
Now, we’re going to rewrite all the previous by composing mini-classes instead and using Typescript mixins to create components made of many separate, small classes.
Let’s create the mixins required to create the component TwitterPostComponent: likeMixin, deleteMixin and shareMixin.
Base Class
First of all, we want the mixins to be generic enough to be applied to a variety of components, with one single dependency being the service injected to the component.
export interface PostComponent {
post: Post;
service: PostService;
}
likeMixin
// like
function likeMixin<IBasePost extends Constructor<PostComponent>>(
Base: IBasePost
) {
return class extends BasePost implements CanLike {
get likes() {
return this.post.likes;
}
like() {
return this.service.like(this.post.id);
}
unlike() {
return this.service.unlike(this.post.id);
}
};
}
deleteMixin
function deleteMixin<IBasePost extends Constructor<PostComponent>>(
BasePost: IBasePost
) {
return class extends BasePost implements CanDelete {
delete() {
return this.service.delete(this.post.id);
}
};
}
shareMixin
*export function shareMixin<IBasePost extends Constructor<PostComponent>>(
BasePost: IBasePost
) {
return class extends BasePost implements CanShare {
shares: number;
share() {
return this.service.share(this.post.id);
}
};
}
Creating the implementation component: TwitterPostComponent
Once created, we can apply them to the newly created TwitterPostComponent:
const TwitterBase = deleteMixin(
likeMixin(
shareMixin(PostComponent)
)
);
If you prefer to use the applyMixins function described in Typescript’s own documentation, you can do the following:
class TwitterBase extends PostComponent {}
interface TwitterBase extends CanLike, CanDelete, CanShare {}
applyMixins(TwitterBase, [
shareMixin,
likeMixin,
deleteMixin
]);
Once created the base component composed with the mixins, we can extend the new component TwitterPostComponent:
@Component({
selector: 'twitter-post',
template: `
<div class="post">
<div class="post-header">
{{ post.author }}
</div>
<div class="post-content">
{{ post.content }}
</div>
<div class="post-footer">
<button (click)="like()">Like</button>
<button (click)="share()">Share</button>
</div>
</div>
`
})
export class TwitterPostComponent extends TwitterBase {}
In order to remove a delete functionality from the Tweets components, we don’t have to do much — we simply remove the deleteMixin mixin from our class:
const TwitterBase = likeMixin(
shareMixin(PostComponent)
)
);
Pitfalls of using Mixins
Mixins are great, but they’re not an infallible tool. While I would still prefer Mixins to multiple inheritance, it is important to understand the implications of using this technique.
This React blog post provides a great explanation on why Mixins are no longer considered a best practice in React:
Mixins create implicit dependencies: mixins that call methods on components, reference a property from the component, or components that need a mixin to work well, are all dependent on each other
Mixins start small but grow over time
Mixins lead to name clashes
Of course, because of the similarities, these also apply to Typescript mixins used with Angular components.
How to avoid these pitfalls?
Try not to apply too many mixins; if you have too many mixins, maybe you should split the component into several components and use component composition with inputs and outputs to communicate between each other
Strive to keep them as small as possible
Keeps dependencies between mixin/component to a minimum. For example, wherever possible, try not to call a component’s dependencies from the mixin
Combine the mixins technique with component composition. Together with the usage of small mixins, you can leverages both techniques to share code and maintain a healthy codebase
Resources
Angular Material is a library that makes use of mixins, so I would suggest you check out their components to see how they can be used in a variety of situations
If you need any clarifications, or if you think something is unclear or wrong, do please leave a comment!
I hope you enjoyed this article! If you did, follow me on* Medium, Twitter or my website for more articles about Software Development, Front End, RxJS, Typescript and more!
Top comments (3)
Awesome article, Giancarlo. Amazing!
One thing I've noticed when extending classes in Angular is that when we have constructor injected dependencies, we have to add a constructor to the top-most level class with the necessary dependencies to make Angular's dependency injection work.
Have you noticed this when using class-based mixins?
Hi Lars, thanks a lot again!
If you use a Mixin, you don't really need to re-inject the dependency, but you do need to tell TS that the class to which the mixin is applied to does have that particular service/type.
For example, Angular Material uses interfaces such as "HasElementRef" (github.com/angular/components/blob...)
Hi Giancarlo, great explanation! Do you also have some code examples of unit tests for the different methods?