Services are one of fundamental blocks of every Angular application. Service is just a TypeScript class with or even without @Injectable decorator.

To create a service all we need to do is create a class

export class VoteService {}

And register it in providers array of @NgModule

import {VoteService} from './vote.service'; ...

@NgModule({

imports: [ BrowserModule],

declarations: [ AppComponent],

bootstrap: [ AppComponent],

providers: [VoteService]

})

The second way (more preferred in Angular 6) is to use @Injectable decorator and specify providedIn property

import { Injectable } from '@angular/core'; @Injectable({

providedIn: 'root',

})

export class VoteService { }

‘root’ means that we want provide the service at the root level (AppModule)

When you provide the service at the root level, Angular creates a single, shared instance of service and injects into any class that asks for it. Registering the provider in the @Injectable metadata also allows Angular to optimize an app by removing the service if it turns out not to be used after all.

So here is where things get more interesting. When you add a service provider to root module (root injector), it is available for whole application. That means if you have a feature module with service in providers and that service is also provided in root module, in this case both modules will work with the same instance of service (singleton pattern). So keep in mind whenever you add a service to root module that service is available in whole application and all components/directives have access to the same instance of the service, unless the feature module is lazy.

So let’s see this with code. We have a VoteService that is provided in root module ( AppModule )

import { Injectable } from '@angular/core'; @Injectable({

providedIn: 'root'

})

export class VoteService {

votes: number = 10; constructor() { } getVotes() {

return this.votes;

} setVotes(vote: number) {

this.votes = vote;

}

}

So here we have a simple service with votes property and two methods. One for getting the value of that property and the other for setting a new value. The initial value is set to 10

Now let’s inject our service in app.component and display the value of votes

@Component({

selector: 'my-app',

template: `app component - {{votes}}`

})

export class AppComponent {

votes: any; constructor(private vt: VoteService) {}



ngOnInit() {

this.votes = this.vt.getVotes()

this.vt.setVotes(25);

}

}

Here we inject our service in constructor, assign the value of votes from service to component variable and set new value. So far in the browser we will see this

app component - 10

10 was the value that was set initially. Now let’s create a feature module (not lazy ) and also provide VoteService

@NgModule({

imports: [CommonModule],

declarations: [FeatureComponent],

exports: [FeatureComponent],

providers: [VoteService]

})

export class FeatureModule { }

This FeatureModule is imported in root module (AppModule). So in FeatureComponent we can also inject VoteService and get the value of votes

@Component({

selector: 'app-fac',

template: `{{votes}}`

})

export class FeatureComponent implements OnInit {

votes: number;

constructor(private vt: VoteService) { } ngOnInit() {

this.votes = this.vt.getVotes()

}

}

Now let’s modify AppComponent template do display FeatureComponent

template: `app component - {{votes}}

<br>

feature component - <app-fac></app-fac>`

As a result we will see

app component - 10

feature component - 25

Why so ? The reason is that both app component and feature component are working with the same instance of VoteService . Let’s see how this is working.

Our modules (both AppModule and FeatureModule) have VoteService in their providers

2. Because we import FeatureModule to AppModule and both have a provider with the same token ( the same service ), AppModule wins. That’s because both providers are added to the same injector.

Angular uses injector system. When a root module is loaded at application launch all providers from all imported modules are added to root injector, that’s why they are accessible throughout the entire application.

When we import two modules that provide the same service, second module always wins, because it was added the last.

3. In app.component when we inject the VoteService Angular starts searching for providers inside that component, then it goes up by hierarchy until it finds it in root module (root injector). After that Angular checks if there is an instance or not. If not new instance will be created and returned to app.component. So that’s why when in feature.component we inject VoteService we actually use already created instance of the class, therefore we get 25 votes instead of initial 10.

Visual representation of provider scope

What about Lazy Modules ?

Things get even more interesting when we use lazy modules. Lazy modules are modules that use lazy loading. That means you load the module only when you really need. For example in your website you have a page about ‘new products’ but users visit that page very rarely. You can split the logic of your application, make a lazy module that will be responsible for that page and with that your application will start even faster because one module will not be loaded until someone visits that page.

To create a lazy module we need to use routing. So let’s configure app routes in AppModule

RouterModule.forRoot([{

path: 'lazy',

loadChildren : './lm/lazy.module#LazyModule'

}])

Here we are saying if the path of URL will be /lazy load this module, and we provide the relative path of our lazy module . Don’t forget to create that module. You can use CLI to generate a module — ng generate module lazy . We do not import lazy modules in the root module.

We also need to configure routes in LazyModule . So inside imports array of LazyModule we can do this

RouterModule.forChild([{

path: '', component: LazyComponent

}])

As a path we are using empty string because in the AppModule we already provided a route. Also notice that instead of forRoot we are using forChild.

That’s it, now we have lazy module and a route to activate that module. Let’s add some buttons that will change the route.

Change the template of app.component

@Component({

selector: 'my-app',

template: `app component - {{votes}}<br>

<button routerLink='lazy'>lazy load module</button>



<button (click)="refresh()">refresh</button>

<router-outlet></router-outlet>

`

})

...

We still display votes count from our service, but we also added 2 buttons, one for changing the route and loading our lazy module and the second for refreshing the votes property.

refresh() {

this.votes = this.vt.getVotes()

}

Finally let’s create a template of our lazy component

...

template: `<br>lazy component - {{votes}}

<button routerLink='/'>back to app component</button>`

...

So in our LazyModule we also provide VoteService . The service is injected in LazyComponent so we can create a votes variable and use it to display vote count from VoteService

This is what will happen.

Initially the value of votes is 10. When we lazy load a module the value is 10 again instead of 25. When we refresh, the value of votes changes only for app.component, and for lazy.component it stays 10. Now let’s see in more detail

We have two modules (AppModule and LazyModule) that provide the same service. When application starts root injector is created with all services from all eagerly modules. However as our module is lazy Angular doesn’t know about the existence of our LazyModule. This means that any service listed in providers array of our LazyModule isn’t available because the root injector doesn’t know about LazyModule… yet. When we activate the route, lazy module will be loaded and new injector created. Imagine a tree of injectors, on top there is the root injector and for each lazy module new child injector will be created. All services from root injector will be added to child injector. If root injector and child injector provide the same service, Angular prefers service instances from child injector. So every lazy component gets the local instance of the service, not the instance in the root application injector. That’s why when we refreshed the state, only votes of app.component changed because lazy.component is working with another instance of the service.

Limiting providers scope with Lazy Modules

So quick recap

If we have several Feature Modules that provide the same service, only the last module’s service will be added to root injector If we have several Feature Modules that provide the same service and also that service is provided in AppModule (root module), only the instance created from AppModule will be added to root injector. So even components of FeatureModules will use the service instance from AppModule If we have Lazy Module that doesn’t provide a service it will use the instance created from root injector (AppModule) But if Lazy Module does have provided a service, components of that module will use local instance of that service (not the instance from root injector)

Limiting provider scope with components

Another way to limit provider scope is to provide a service in a providers array inside @Component decorator. Component providers and NgModule providers are independent of each other. Providing a service in the component limits the service only to that component and components inside that component (component tree)

@Component({

...

providers: [VoteService]

})

Thanks for reading. Hopefully now it’s not so complicated. The full code is available here