Demystifying Angular Services and Dependency Injection

yeshasmp

Yeshas M P

Posted on April 16, 2022

Demystifying Angular Services and Dependency Injection

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 -

  1. 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.
  2. It’s easier to test and debug.
  3. 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);
  }
}

Enter fullscreen mode Exit fullscreen mode

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");
  }
}
}

Enter fullscreen mode Exit fullscreen mode

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 -

1) 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'
})

Enter fullscreen mode Exit fullscreen mode

2) 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]

Enter fullscreen mode Exit fullscreen mode

3) By manually registering using @Inject inside constructor of consumer.

constructor(@Inject(AppService) private appService)

Enter fullscreen mode Exit fullscreen mode

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]

Enter fullscreen mode Exit fullscreen mode

which is shorthand property when expanded would look like

providers : [{ provide : AppService , useClass : AppService }]

Enter fullscreen mode Exit fullscreen mode

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 -

Hierarchical Injection

  1. At runtime, Angular resolves dependency by following hierarchical injection tree. An injection tree is nothing but tree of injector instances.

  2. 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 an Element Injector tree which holds injectors of all components and directives.

  3. When AppComponent requests AppService Angular DI system at first will look at the provider array of AppComponent using the injection token given in the constructor.

  4. 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 in Element Injector tree.

  5. If no providers are found in Element Injector tree then it searches in Module Injector tree. If the requested component is under lazy loaded module it searches in the provider of Lazy Loaded Module injector before proceeding to the Root Module injector.

  6. 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 and Module Injector trees it reaches Null injector and throws NullInjectorError as shown below.

Angular service null injector error

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)

Enter fullscreen mode Exit fullscreen mode

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;
  }
}

Enter fullscreen mode Exit fullscreen mode

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();
  }
}

Enter fullscreen mode Exit fullscreen mode

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();
  }

}

Enter fullscreen mode Exit fullscreen mode

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.

Angular singleton service

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.

Angular non-singleton service

But how did two instance of Angular services got created?

Angular Hierarchical Injection

  1. When AppComponent requested the AppService, Angular looked for it in AppComponent provider at first, when it couldn’t find it it went into Module injector tree and found the AppService in Root Module provider and returned it to AppComponent.

  2. Next when HomeComponent requested AppService it found it in the HomeComponent provider itself and returned new AppService instance to HomeComponent.

  3. Therefore we saw two instance of AppService being created and provided to respective components.

Few points to remember before we conclude -

  1. Element Injector tree always gets preference over Module Injector tree and it is not child of Module Injector tree.

  2. 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.

  3. Service which are provided at Root Module or Eagerly Loaded Module are app scoped and accesible to all the components or directives. Services which are provided in Lazy Loaded Module are module scoped and available only to the components or directives under that module.

  4. Proivder holds the list of dependencies with it’s matching token, while Injector holds provider itself.

  5. If two Eagerly Loaded modules providers has service for same injector token then the module which is imported at last in Root 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!

💖 💪 🙅 🚩
yeshasmp
Yeshas M P

Posted on April 16, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related