In any Angular application it is essential to keep component as lean as possible, it’s only concern should be on how to present the model data to view as described by Angular official docs.
“a component’s job is to enable the user experience and nothing more.”
where other logics such as fetching data from API endpoint or handling client and server side errors should be taken care by services.
Angular Services
Angular services are simple class which is used to perform specific functions. Angular Services offer several advantages -
- It’s easier to write logic once in service and share the service among the components instead of writing the same logic in every component.
- It’s easier to test and debug.
- It’s easier maintain and perform code updates when required.
Angular Service Example
We can generate Angular Service in Angular CLI using ng g s AppService
where ‘g’ and ’s’ is shorthand form for ‘generate service’.
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class AppService {
constructor() { }
alertMsg(msg : string) {
alert(msg);
}
}
Above service has method to launch alert popup with custom message. AppComponent
can request AppService
in it’s constructor and call the alertMsg method as shown below.
import { Component } from '@angular/core';
import { AppService } from '../app.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
constructor(private appService:AppService){
this.appService.alertMsg("App Component launched");
}
}
}
By default, Angular services are singleton. When services are registered either in provider array of root module or with providedIn value of ‘root’ or ‘any’ in the service itself there’s only single instance of service available throughout the application.
We understood how Angular service could be called in any component, but did you wonder how did AppComponent
got an instance of AppService
? For any class to execute another class method it needs to create an object of that class and call the method through it unless it’s static. But where did AppComponent
create any instance of AppService
?
Let’s move into next section to know how AppComponent
got an instance of AppService
.
Dependency Injection
When AppComponent
get’s loaded, Angular would create and provide an instance of AppService
to the AppComponent
giving access to alertMsg
method. This process is known as Dependency Injection
. As stated in Wikipedia
“In software engineering, dependency injection is a technique in which an object receives other objects that it depends on, called dependencies.”
In Angular terms it’s “Responsibility of Angular framework to create an instance of service and provide it to the requested component”. The requested component need not know how and where to create service instance, it can simply request in it’s constructor and Angular would provide it.
Services needs to register itself as dependency before any component can request it. There are 3 ways where service can register itself as dependency -
- Using providedIn property inside the
@Injectable
decorator of the service class itself. This is preferred way of registering a service as stated by Angular Docs since it tree shakable meaning Angular would include this service during build time if and only any component requests it. Otherwise this is excluded from the build which helps in improving the performance of our app.
@Injectable({
providedIn: 'root'
})
- By registering in provider array at Module level, Component level or Directive level. Service provided like below is not tree shakable and would be included in the build even if no component requests it.
providers: [ AppService ]
- By manually registering using
@Inject
inside constructor of consumer.
constructor(@Inject(AppService) private appService)
A Provider
is an object which holds list of all services registered in provider array. Angular creates provider and injector instance for root module and for each lazy loaded module. It also creates injector instance for all components and directives. Each Injector
holds the provider list of all dependencies registered at respective component, directive or modules.
Note - Angular doesn’t create injector instance for Eagerly Loaded modules
therefore, the services registered at those modules will be configured in Root Module injector
.
Typical service when registered in provider array at module or component would look like below -
provders : [AppService]
which is shorthand property when expanded would look like
providers : [{ provide : AppService , useClass : AppService }]
provide
property holds the injection token while provider
property holds the instruction on how to create the dependency. Injection token can either be a type, a string or an injection token itself. We can not only provide class as dependency but also provide direct value or value returned from function or function itself using useValue
, useFactory
and useExisting
provider properties. Visit Angular Docs to know more how you use other provider types.
Now let’s breakdown how Angular would resolve the dependency using provider and injector in below steps for better understanding -
At runtime, Angular resolves dependency by following hierarchical injection tree. An injection tree is nothing but tree of injector instances.
By default, Angular creates
Module Injector tree
having one root module injector and seperate module injector for each lazy loaded module. At the top of root module injector sits Null and Platform module injectors. It also creates anElement Injector tree
which holds injectors of all components and directives.When
AppComponent
requestsAppService
Angular DI system at first will look at the provider array ofAppComponent
using the injection token given in the constructor.If no provider is found in the
AppComponent
injector, then it traverse upto the parent components in search of matching provider using token until it reaches the root component injector inElement Injector tree
.If no providers are found in
Element Injector tree
then it searches inModule Injector tree
. If the requested component is under lazy loaded module it searches in the provider ofLazy Loaded Module injector
before proceeding to theRoot Module injector
.When provider is found, it creates an instance of service and provide it to the requested component. If no provider is found in both
Element Injector
andModule Injector
trees it reaches Null injector and throwsNullInjectorError
as shown below.
We can control the dependency resolution using @Skip, @SkipSelf, @Optional and @Host resolution modifiers. We can avoid the above null injector error when dependency is tagged with @Optional
modifier in the requested AppComponent
constructor like below. Then Angular would return null instead of throwing error.
constructor(@Optional private appService : AppService)
Are Angular Services singleton?
Let’s consider below code scenario to understand Hierarchical injection of services and whether Angular services are singleton or not. Head to Stackblitz to experiment and play below code example.
We will create an AppService
which generates random number when it’s instance is created and return that value through method. Initially we will register AppService
only in root module using providedIn
value as ‘root’ -
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class AppService {
sharedValue : number;
constructor() {
this.sharedValue = Math.floor(Math.random() * 5);
console.info("AppService instance has been created!")
}
getSharedValue(){
return this.sharedValue;
}
}
Let’s create two more components - AppComponent
and HomeComponent
a child of AppComponent
and request the AppService
in both the component constrcutor.
AppComponent
-
import { Component } from '@angular/core';
import { AppService } from './app.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
appServiceValue : any;
constructor(private appService : AppService) {
this.appServiceValue = this.appService.getRandomNumber();
}
}
HomeComponent
-
import { Component, OnInit } from '@angular/core';
import { AppService } from '../app.service';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.css'],
//providers: [AppService]
})
export class HomeComponent {
appServiceValue : any;
constructor(private appService : AppService) {
this.appServiceValue = this.appService.getRandomNumber();
}
}
We will then display the random number in both app and home component by calling getRandomNumber method and passing the value to view. When we load the application we can see both components get the same random number as they both received same single instance of AppService
from Root Module injector
. This proves Angular services are singleton when they are registered only in Root Module provider
.
Now let’s register AppService
also in HomeComponent
provider and run the app. App and Home component displays two different random numbers because they both received two different instances of AppService
. Thus we can say Angular services are not singleton when they are provided at different levels.
But how did two instance of Angular services got created?
When
AppComponent
requested theAppService
, Angular looked for it inAppComponent
provider at first, when it couldn’t find it it went intoModule injector tree
and found theAppService
inRoot Module provider
and returned it toAppComponent
.Next when
HomeComponent
requestedAppService
it found it in theHomeComponent
provider itself and returned newAppService
instance toHomeComponent
.Therefore we saw two instance of
AppService
being created and provided to respective components.
Few points to remember before we conclude -
Element Injector tree
always gets preference overModule Injector tree
and it is not child ofModule Injector tree
.Angular DI resolves dependencies using bottom to top approach, it starts the search for provider first from the requesting component and then traverse upto the parent components to the
Root Module provider
.Service which are provided at
Root Module
orEagerly Loaded Module
are app scoped and accesible to all the components or directives. Services which are provided inLazy Loaded Module
are module scoped and available only to the components or directives under that module.Proivder
holds the list of dependencies with it’s matching token, whileInjector
holds provider itself.If two
Eagerly Loaded modules
providers has service for same injector token then the module which is imported at last inRoot Module
gets preference.
Above code example have been shared over Github and Stackblitz.
That’s it folks! I hope this article helped you understand better how Angular Dependency works and how Angular Services are singleton by nature.
Stay tuned for more such interesting articles!