Stop using inheritance in Angular
At Simplified Courses, we tend to do a lot of Angular code-reviews for companies that want to make sure they are still on the right track. One of the principles that often makes their codebase unmaintainable and unnecessarily complex is the principle of inheritance. This article explains why we should stay away from inheritance in general and what the dangers are when using inheritance. After that, we will cover an elegant alternative.
Why do people use inheritance in the first place?
At school, we are taught to think DRY. DRY stands for “Don’t Repeat Yourself”. This means we shouldn’t repeat any code so we have to make sure if we have 2 classes that need the same code, that this code isn’t duplicated. In some cases, KISS (“Keep it Simple Stupid”) is more important than DRY so we still duplicate pieces of code in favor of flexibility. In this article we will focus on DRY and how to keep our codebase DRY. The first thing that they taught us at school was to use inheritance, where we extracted the duplicated logic that we needed to share in a base class. Take this code for instance:
class Car {
public start(): void {...}
public stop(): void {...}
...
}
class Bike {
public start(): void {...}
public stop(): void {...}
...
}
We can see that both of these classes have a start()
method and stop()
method implemented. This functionality is redundant (duplicated) so it is not DRY.
We can fix this by creating a Vehicle
base class that implements the start()
and stop()
method:
class Vehicle {
public start(): void {...}
public stop(): void {...}
}
class Car extends Vehicle{
...
}
class Bike extends Vehicle {
...
}
As we can see, the Car
class and the Bike
class, both extend from that base class and fix the duplicate logic issue that we had before. The nice thing is, our logic is in one central place and by inheriting from that base class we get this logic for free.
However, we consider this a bad practice and we will explain why.
Why we should not use inheritance in Angular (or any technology for that matter)?
Inheritance is a bad practice in general because the classes that inherit from base classes tend to inherit more than they need. When we inherit from a base class we inherit everything from that base class so inheritance:
- Breaks single responsibility pattern
- Is hard to unit-test, hard to mock
- Breaks separation of concerns
- Can become very complex in no-time
- Hard to read, we have no clue what kind of logic our base class has
- Inheritance is the strongest form of coupling. Changing the base class can break inheriting classes.
Let’s say that we have a class Foo
that needs the following functionalities:
- Logging
- Data fetching
- Caching
We could inherit the class Foo
from Base
like this and every instance of Foo
would have the three functionalities
export class Base {
log() {...}
fetch() {...}
cache() {...}
}
export class Foo extends Base {
// acccess to logging, data fetching and caching
}
Imagine we now have a class Bar
that needs logging, data fetching but it does not need caching. However it also needs access to the authentication, so now this class needs:
- Logging
- Data fetching
- Authentication
What could add the authentication part to the Base
class as well and let Bar
inherit from Base
like this:
export class Base {
log() {...}
fetch() {...}
cache() {...}
authenticate() {...}
}
export class Foo extends Base {
// acccess to logging, data fetching and caching
}
export class Bar extends Base {
// acccess to logging, data fetching and authentication
}
Our Base
class is becoming dirtier because now it has 4 functionalities that are unrelated and Foo
has access to authenticate()
when it doesn’t need to, while Bar
has access to cache()
when it doesn’t need to. This clearly breaks the single responsibility pattern and when our application grows, the Base
class would grow in functionality and no one would even know what is inside that Base
class. By pushing inheritance further we could wind up with base classes inheriting from other base classes and so on.
Wouldn’t this snippet make more sense?
export class Logger {
log() {...}
}
export class DataAccess {
fetch() {...}
}
export class Cacher {
cache() {...}
}
export class Authenticator {
authenticate() {...}
}
export class Foo {
// acccess to logging, data fetching and caching
private logger = inject(Logger);
private dataAccess = inject(LoDataAccessgger);
private cacher = inject(Cacher);
}
export class Bar {
// acccess to logging, data fetching and authentication
private logger = inject(Logger);
private dataAccess = inject(LoDataAccessgger);
private authenticator = inject(Authenticator);
}
This is easy to mock out, breathes single responsibility and is easy to read. No magic happening in a base class here. This is clearly a better approach, since we both Foo
and Bar
only have access to functionalities they actually need, and it is very composable.
Composition
Composition is the principle where we group pieces of related code in classes and use instances of those classes in the class rather than letting the class inherit from a base class. In Angular, we can leverage Dependency Injection for that.
We should always use composition unless the following statement is true: There is one reason, and one reason only why we could use inheritance in Angular projects: When all the child classes need all the functionality of our base class.
Dependency injection in Angular
To use the composition pattern in Angular, we can leverage Dependency Injection. The idea behind dependency injection is that we register a dependency to our framework and when we ask for that dependency the framework gives us an instance of that dependency.
// Create an injectable dependency
@Injectable()
export class UserService {
...
}
@Component({
...
// Register that injectable dependency to the root component
// so it becomes a singleton
providers: [UserService]
})
export class AppComponent {...}
@Component({...})
export class UserListComponent {
// Inject an instance of that injectable wherever we like
private readonly userService = inject(UserService);
}
This means we can split up all the logic into different injectables and inject them/mock them wherever we want. This makes it easy to pick different pieces of functionality from wherever we see fit.
What about components?
When providing injectables in the root of the application we create singleton instances. Having singleton instances for every dependency might not always be a good idea. Sometimes we want new instances of an injectable every time a component is created. Sometimes the lifecycle of an Angular injectable needs to be shared with the lifecycle of a component.
The power of instances per component
Let’s say that we want to share phonenumber functionality between this AddUserComponent
and an EditUserComponent
:
@Component({...})
export class AddUserComponent {
public phoneNumbers: string[] = [];
public addPhoneNumber(): void { ... }
public removePhoneNumber(): void { ... }
public updatePhoneNumber(): void { ... }
...
}
@Component({...})
export class EditUserComponent {
public phoneNumbers: string[] = [];
public addPhoneNumber(): void { ... }
public removePhoneNumber(): void { ... }
public updatePhoneNumber(): void { ... }
...
}
This is redundant logic and a singleton will not fulfill our needs since it has a piece of state called phoneNumbers
. We could let them both extend from a base class but we have already seen the downsides of that approach. Instead, let’s create an injectable called PhoneNumberState
that has all that functionality and inject it in both the AddUserComponent
and EditUserComponent
. By adding it to the providers
property of both components we will create 2 instances that will live and die along with their connected components and we won’t have to share state between them.
If we would have extended from PhoneNumberState
we would have to add all the shared logic in PhoneNumberState
which would make it lose its single responsibility.
// Create an injectable
@Injectable()
export class PhoneNumberState {
public phoneNumbers: string[] = [];
public addPhoneNumber(): void { ... }
public removePhoneNumber(): void { ... }
public updatePhoneNumber(): void { ... }
}
@Component({
...
// Provide it for the first time
// (connected to AddUsercomponent)
providers: [PhoneNumberState]
})
export class AddUserComponent {
// Inject the instance connected to AddUserComponent
private readonly phoneNumberState = inject(PhoneNumberState);
...
}
@Component({
...
// Provide it for the first time
// (connected to EditUserComponent)
providers: [PhoneNumberState]
})
export class EditUserComponent {
// Inject the instance connected to EditUserComponent
private readonly phoneNumberState = inject(PhoneNumberState);
...
}
We now have used composition to avoid duplicated code and when the AddUserComponent
gets destroyed the instance of PhoneNumberState
connected to that instance will be destroyed, and when the instance of EditUserComponent
gets destroyed, the instance of PhoneNumberState
connected to that instance will be destroyed.
We can even implement the ngOnDestroy()
lifecycle hook on PhoneNumberState
if we like:
@Injectable()
export class PhoneNumberState implements OnDestroy {
public phoneNumbers: string[] = [];
public addPhoneNumber(): void { ... }
public removePhoneNumber(): void { ... }
public updatePhoneNumber(): void { ... }
public ngOnDestroy(): void {
// Add some teardown logic here
}
}
Now for AddUserComponent
and EditUserComponent
the instances would get destroyed when their component that provides them gets destroyed.
Sharing state between child components
Sometimes we want to share state/logic with child components and communicating with @Input()
properties and @Output()
properties is not possible. Let’s take a wizard component for instance that has 3 steps. This would result in:
- A
WizardComponent
as a parent component - A
Step1Component
as a child component ofWizardComponent
- A
Step3Component
as a child component ofWizardComponent
- A
Step3Component
as a child component ofWizardComponent
We would want to share state between the child components but we also want to destroy the injectable when the wizard is completed (for instance when we navigate away from that wizard). In that case we can just provide the Wizard
on the WizardComponent
but inject and consume it in all the child components:
// Create an injectable with Wizard state and functionality
@Injectable()
export class Wizard {...}
@Component({
...
// Provide it once on the parent component
providers: [Wizard]
})
export class WizardComponent { ... }
@Component({...})
export class Step1Component {
// Inject it in Step1Component
private readonly wizard = inject(Wizard);
}
@Component({...})
export class Step1Component {
// Inject it in Step3Component
private readonly wizard = inject(Wizard);
}
@Component({...})
export class Step1Component {
// Inject it in Step3Component
private readonly wizard = inject(Wizard);
}
The WizardComponent
, Step1Component
, Step2Component
and Step3Component
all share the same instance of the Wizard
injectable. Which is nice for reusing state and functionality and the moment we navigate away from the WizardComponent
the state would automatically be destroyed.
If we don’t want to destroy that state, we could provide it on the AppComponent
or use providedIn: 'root'
:
@Injectable({ providedIn: 'root'})
export class Wizard {...}
Summary
We learned that inheritance can be dangerous and it makes our codebase hard to scale. Composition is better because it follows the single responsibility pattern, it is more readable, more testable and easier to understand.
Angular has a great Dependency Injection system that we can use to create singletons. But we can also create instances of a dependency on all the different component levels which gives us a lot of flexibility. In our opinion, it’s better to use composition over inheritance.
We can provide a dependency:
- As a singleton: with
providedIn: 'root'
- As a singleton: At the
providers
property of the root component - Tied to a component: At the
providers
property of any component. This makes it extremely handy to let the instances destroyed automatically when the component that provides the instance gets destroyed.
It’s important to remember that when providing a dependency on a component we can implement the ngOnDestroy()
lifecycle hook.
If you want to test your knowledge on the topic: “Angular Dependency injection”, check out this free Angular Dependency Injection Quiz.
Note: As a side note, we also might want to check HostDirectives which can be used for composition as well
Special thank you to the reviewers
These awesome people helped me review this article. Thanks!!
If you like to learn directly from me, check out my Angular Training and Angular Coaching
Follow @TwitterDevJoin the club and get notified with new content
Get notified about our future blog articles by subscribing with the button below.
We won't send spam, unsubscribe whenever you want.
Update cookies preferences