Skip to main content

8. Understanding Angular's Dependency Injection

In this section, we focus on Angular's dependency injection system.

We'll start with a look at dependency injection and how we tell Angular that we want a specific service.

1. Dependency Injection Overview

Before going in to more ways of defining/registering/consuming a service, we must have an overall understanding of how Angular works things out.

1.1. Defining a service

When we define and export a service, this is syntactically the same as a constructor function

@Injectable()
export class CustomService {
...
}

1.2. Registering a service

We need to register an instance of our defined service. Behind the scene, Angular takes the service class and creates an instance of it for our use.

@NgModule({
providers: [ CustomService ]
...
)}
export class AppModule { }

1.3. Consuming a service

To lookup a service, Angular uses the class defined CustomService.

The declaration type of our class serves as a key and Angular will look inside its Dependecy Injection registry to find the correct instance to inject.

import { Component } from '@angular/core'
import { CustomService } from './custom.service';

@Component({
templateUrl: './profile.component.html'
})
export class ProfileComponent {

constructor(private customService: CustomService) { }
}

2. Defining and registering a service with Angular's Injection token

If we take a look at the Toastr Service we've created previously, it has some inconveniences. First, we are defining an entire new class to wrap an existing service. This is fine in this example but for a more complex 3rd party library, we don't want to rewrite all wrappers methods of our service. ⚠️ the toastr object doesn't really need a wrapper service, so is using globals, we must find a new way to inject our service properly.

💡 If we want to deal with things that aren't simple services we must procede differently. To register other kinds of services with the dependency injection system we use a combination of the InjectionToken and the @Inject decorator.

We start by the definition of our service

import { InjectionToken } from '@angular/core';

export let TOASTR_TOKEN = new InjectionToken<Toastr>('toastr'); // description string used as 1st parameter

export interface Toastr {
success(message: string, title?: string): void;
info(message: string, title?: string): void;
warning(message: string, title?: string): void;
error(message: string, title?: string): void;
}

Next we need to register the service using the token we created previously.

import { TOASTR_TOKEN, Toastr } from './common/toastr.service';
declare let toastr: Toastr;

@NgModule({
providers: [
{
provide: TOASTR_TOKEN,
useValue: toastr
}
]
...
)}
export class AppModule { }

3. Consuming a service with the @Inject decorator

To consume our service we use the @Inject decorator and pass in our newly created token in the constructor of our consuming class.

import { Component, Inject } from '@angular/core'
import { TOASTR_TOKEN, Toastr } from './common/toastr.service';

@Component({
templateUrl: './profile.component.html'
})
export class ProfileComponent {

constructor(@Inject(TOASTR_TOKEN) private toastr: Toastr) { }

saveProfile() {
this.toastr.success('Profile saved!');
}
}

4. The useClass Provider

There are alternate methods we can use besides useValue to inject a service. By default, when registering a service, we are in fact using useClass under the hood.


@NgModule({
providers: [ CustomService ]
...
)}
export class AppModule { }

The following code is equivalent to the previous example:


@NgModule({
providers: [
{
provide: CustomService, useClass: CustomService
}
]
...
)}
export class AppModule { }

💡 The useClass provider comes in handy when we want to inject a specific implementation of a class, for example:


@NgModule({
providers: [
{
provide: Logger, useClass: FileLogger
}
]
...
)}
export class AppModule { }

5. The useExisting Provider

This useExisting provider is also know as the "alias provider"


@NgModule({
providers: [
{
provide: MinimalLogger, useExisting: Logger
}
]
...
)}
export class AppModule { }

💡 This can be useful when we want to minize an API. For example, if we have a huge Logger service with 30 methods in it but we are only using 5 of them.

6. The useFactory Provider

This useFactory provider is used to provide a factory function


@NgModule({
providers: [
{
provide: Logger, useFactory: factory()
}
]
...
)}
export class AppModule { }

💡 This can be useful if we want to parameterize the creation of a specific service etc.