Understand Angulars Hierarchical Dependency Injection system blog post

Understand Angulars Hierarchical Dependency Injection system

Cory Rylan

31 May, 2019

Angular

9 minutes read

Angular is well known for its robust dependency injection system. Using dependency injection has many benefits, including more straightforward testing strategies and dependency management in our applications. With Angular’s dependency injection system, we can create special classes called services that allow us to share logic and data between components and features. In this post, we will look at how we can take advantage of Angular’s advanced hierarchical dependency injection to create services that can are created multiple times or for specific features of our application.

Dependency Injection

Angular’s dependency injection system is hierarchical. A hierarchical dependency injection system allows us to define different boundaries or scopes for our dependencies to run in and follows the component tree structure. By default, services registered to Angular are application wide but we can also create services that are isolated to a subset of components. Our first example will show a basic service that we typically see in an Angular application.

Application-Wide Singleton Services

Typically when using Angular services, we think of services as being an application-wide singleton. Singleton services by default in Angular mean that Angular creates one instance of our service and shares that instance to all the components in our application. Let’s take a look at an example of how this works.

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

@Injectable({
  providedIn: 'root' 
})
export class MessageService {
  messages = [
    '10 rockets built',
    'new configurations available'
  ];

  addMessage(message: string) { ... }
}

By default, when we create a service with the Angular CLI, we get something similar to the code example above. On our service class, we have the @Injectable decorator letting Angular know that other components can inject and use this service. In the decorator, the providedIn property value is root. By setting the providedIn property to root Angular registers the service to the root injector. When a service registers to the root injector, it allows the service to be used application wide.

By registering services application wide, we can easily share the services and any logic contained within them. This can also be useful for sharing state or data across our entire application within multiple components. Singleton services work great for a large majority of tasks in Angular applications. Sometimes though we may want to adjust this default behavior. With Angular, we have a few options.

For example, what if we want to create multiple instances of the same service? Why would we want this? In our next case, we will see how we can create component level service instances.

Component Level Services

In our use case example, we are building out a UI for ordering rockets. We want to be able to compare and contrast the prices of different rockets based on what options we select (and yes, the rocket prices are almost real!). Here is a screenshot of our prototype UI.

Example UI for Angular Component Services

Each time we click to add a rocket, we create a new Rocket order where we can adjust and build our rocket. Each setting changes the price of the rocket and updates it in the UI.

To calculate the cost of the rocket, we have a RocketOrderService that uses an RxJS Observable to emit an updated value whenever the rocket data has changed. This Observable allows any subscribed component to receive those updates.

Let’s take a look at the RocketOrderService:

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

export interface Rocket {
  color: string;
  boosterCondition: number;
  boosterCount: number; 
  total: number;
}

const initialRocket: Rocket = {
  color: '#000000',
  boosterCondition: 0,
  boosterCount: 1,
  total: 60000000
};

@Injectable()
export class RocketOrderService {
  private readonly _rocketChanges = new BehaviorSubject<Rocket>(initialRocket);
  readonly rocket = this._rocketChanges.asObservable();

  updateColor(color: string) {
    const rocket = { ...this._rocketChanges.value, color };
    this.calculateTotal(rocket);
    this._rocketChanges.next(rocket);
  }

  updateBoosterCondition(boosterCondition: number) {
    const rocket = { ...this._rocketChanges.value, boosterCondition };
    this.calculateTotal(rocket);
    this._rocketChanges.next(rocket);
  }

  updateBoosterCount(boosterCount: number) {
    const rocket = { ...this._rocketChanges.value, boosterCount };
    this.calculateTotal(rocket);
    this._rocketChanges.next(rocket);
  }

  private calculateTotal(rocket: Rocket) {
    rocket.total = 60000000;

    if (rocket.color !== '#000000') {
      rocket.total = rocket.total + 10000;
    }

    if (rocket.boosterCondition === 1) {
      rocket.total = rocket.total - 10000000;
    }

    if (rocket.boosterCount === 3) {
      rocket.total = rocket.total + 40000000;
    }

    return rocket.total;
  }
}

Our RocketOrderService is a reactive data service that allows any component to subscribe for updates and changes to our rocket state. Whenever we update one of the rocket values, the total is recalculated and emit the new rocket value via an RxJS Observable.

Using Angular’s dependency injection system we can provide an instance of our RocketOrderService for each instance we have our of our RocketOrderComponent.

Let’s take a look at the RocketOrderComponent:

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder } from '@angular/forms';
import { Observable } from 'rxjs';

import { Rocket, RocketOrderService } from './../rocket-order.service';

@Component({
  selector: 'app-rocket-order',
  templateUrl: './rocket-order.component.html',
  providers: [RocketOrderService]
})
export class RocketOrderComponent {
  rocket: Observable<Rocket>
  total: number = 10;
  form: FormGroup;

  constructor(private rocketOrderService: RocketOrderService, private formBuilder: FormBuilder) {
    this.rocket = this.rocketOrderService.rocket;
  
    this.form = this.formBuilder.group({
      color: ['#000000'],
      boosterCondition: [0],
      boosterCount: [1]
    });

    // when the user updates the form, update the rocket data in the service
    this.form.valueChanges.subscribe(value => {
      this.rocketOrderService.updateBoosterCondition(+value.boosterCondition);
      this.rocketOrderService.updateBoosterCount(+value.boosterCount);
      this.rocketOrderService.updateColor(value.color);
    });
  }
}

In the component decorator, we have a new property providers. The providers property contains anything that we want to make available to inject for the particular component. By adding the RocketOrderService to the providers on our component, Angular’s creates a single instance of that service each time it creates an instance of the RocketOrderComponent.

Not only do we have an instance for each RocketOrder component, but that instance is also shared with any of the child components of the RocketOrder component. This behavior is why Angular’s dependency injections system is hierarchical. Where the provider is defined determines the scope that is available for the components. Let’s take a look at the RocketOrderComponent template.

<form [formGroup]="form" (ngSubmit)="log()" [style.border-color]="(rocket | async)?.color">
  <h3>Rocket Order 🚀</h3>

  <label for="color">Color 🎨</label>
  <input formControlName="color" type="color" id="color"/>

  <label for="booster-condition">Booster Condition</label>
    <select formControlName="boosterCondition" id="booster-condition">
    <option value="0">New</option>
    <option value="1">Used</option>
  </select>

  <label for="booster-count">Number of Boosters ⚡</label>
  <select formControlName="boosterCount" id="booster-count">
    <option value="1">Standard Single</option>
    <option value="3">Tribple Heavy</option>
  </select>

  <app-rocket-total></app-rocket-total>
</form>

Notice how we don’t pass the rocket data into the app-rocket-total component via an Input property. Because we registered our RocketOrderService to the RocketOrderComponent, the RocketOrderComponent and all the child components can inject the service instance.

If we look at the app-rocket-total, we can see this in action:

import { Component } from '@angular/core';
import { Observable } from 'rxjs';

import { Rocket, RocketOrderService } from './../rocket-order.service';

@Component({
  selector: 'app-rocket-total',
  template: `<h3>Total: {{(rocket | async)?.total | currency}}</h3>`
})
export class RocketTotalComponent {
  rocket: Observable<Rocket>;

  constructor(private rocketOrderService: RocketOrderService) {
    this.rocket = this.rocketOrderService.rocket;
  }
}

Using component level services, we can share state and logic between isolated branches of components. Now every time we create a new RocketOrderComponent it and the RocketTotalComponent share the same instance of RocketOrderService.

Note that there is a tradeoff with this pattern of sharing data between components instead of using Inputs and Outputs. It was easier to share data between the components but they are now tightly coupled to the data source (RocketOrderService) meaning they are more difficult to reuse elsewhere in our application.

Now that we have covered application-wide services and component level services we can cover our final way of isolation for services via NgModule.

NgModule Feature Services

We now know how we can share services application wide and isolate them to specific components, but there is a third option at our disposal. Using lazily loaded feature modules, we can separate services to only be available in a given feature. This isolation only works if the NgModule is loaded lazily.

Just like our components using NgModule we can scope service instances to a subset of our application. In our example app, we have two features, the rocket order form and an about page. Each feature is lazy loaded using NgModules and the Angular Router.

export const routes: Routes = [
  { path: '', loadChildren: './rockets/rockets.module#RocketsModule' },
  { path: 'about', loadChildren: './about/about.module#AboutModule' },
];

With each feature, we register a MessageService.

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

@Injectable({
  providedIn: 'root' 
})
export class MessageService {
  value = Math.random();
}

The message service does not have any exciting functionality but helps us understand the behaviour of how Angular creates it.

In each feature module, we register the MessageService to the module providers:

// About Feature Module
@NgModule({
  imports: [
    CommonModule,
    RouterModule.forChild(routes)
  ],
  declarations: [AboutComponent],
  providers: [MessageService] // register the message service
})
export class AboutModule { }

And again:

// Rocket Feature Module
@NgModule({
  imports: [
    CommonModule,
    ReactiveFormsModule,
    RouterModule.forChild(routes)
  ],
  declarations: [
    RocketComponent,
    RocketOrderComponent,
    RocketTotalComponent
  ],
  providers: [
    MessageService // register message service
  ]
})
export class RocketsModule { }

Because we register the MessageService to the feature module Angular will create a single instance of the MessageService for that feature module to use.

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

import { MessageService } from './../message.service';

@Component({
  selector: 'app-about',
  template: `
    <p>Message Service Instance (About Module): {{message}}</p>
    <p>about works!</p>
  `
})
export class AboutComponent {
  message: number;

  constructor(private messageService: MessageService) {
    this.message = this.messageService.value;
  }
}

If we view the about page, we can see the random value is different than the value created by the application wide MessageService.

Example UI for Angular Feature Module Services

By leveraging lazily loaded feature modules, we can create services that are isolated and retained within only that given feature. Module level providers are beneficial if we want to make sure a service is available only within a specific feature, or we want that state to persist in only that feature module.

We covered the three main ways to register services in Angular, root application, component level, and lazy loaded feature level modules. By taking advantage of these techniques, we can safely isolate the responsibilities and state of large Angular applications.

If you want to learn more about Angular’s dependency injection system take a look at the documentation found here. Check out the full working demo application below!

https://stackblitz.com/edit/angular-wbryye

About the author

Cory Rylan profile picture

Cory Rylan

GDE Google Developer Expert

Cory Rylan is a Google Developer Expert and Front End Developer for VMware Clarity. He enjoys building fast progressive web applications and reusable design systems with Web Components and Angular. He also enjoys teaching via workshops, conference speaking, and writing.

Love the post? Share it!

Lots of time and effort go into all our blogs, resources and demos,
we'd love it if you'd spare a moment to share them!

Explore our Angular courses

Get started today and join over 60,000 developers.