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
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)
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
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
This FeatureModule is imported in root module (AppModule). So in FeatureComponent we can also inject VoteService and get the value of votes
Now let’s modify AppComponent template do display FeatureComponent
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.
1. 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.
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
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
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.
Finally let’s create a template of our lazy component
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
1. We have two modules (AppModule and LazyModule) that provide the same service.
2. 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.
3. 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.
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
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)
Thanks for reading. Hopefully now it’s not so complicated. The full code is available here