W3cubDocs

/Angular 7

Hierarchical Dependency Injectors

The Angular dependency injection system is hierarchical. There is a tree of injectors that parallels an app's component tree. You can reconfigure the injectors at any level of that component tree.

This guide explores this system and how to use it to your advantage. It uses examples based on this live example.

Where to configure providers

You can configure providers for different injectors in the injector hierarchy. An internal platform-level injector is shared by all running apps. The AppModule injector is the root of an app-wide injector hierarchy, and within an NgModule, directive-level injectors follow the structure of the component hierarchy.

The choices you make about where to configure providers lead to differences in the final bundle size, service scope, and service lifetime.

When you specify providers in the @Injectable() decorator of the service itself (typically at the app root level), optimization tools such as those used by the CLI's production builds can perform tree shaking, which removes services that aren't used by your app. Tree shaking results in smaller bundle sizes.

You're likely to inject UserService in many places throughout the app and will want to inject the same service instance every time. Providing UserService through the root injector is a good choice, and is the default that the Angular CLI uses when you generate a service for your app.

Platform injector

When you use providedIn:'root', you are configuring the root injector for the app, which is the injector for AppModule. The actual root of the entire injector hierarchy is a platform injector that is the parent of app-root injectors. This allows multiple apps to share a platform configuration. For example, a browser has only one URL bar, no matter how many apps you have running.

The platform injector is used internally during bootstrap, to configure platform-specific dependencies. You can configure additional platform-specific providers at the platform level by supplying extraProviders using the platformBrowser() function.

Learn more about dependency resolution through the injector hierarchy: What you always wanted to know about Angular Dependency Injection tree

NgModule-level providers can be specified with @NgModule() providers metadata option, or in the @Injectable() providedIn option (with some module other than the root AppModule).

Use the @NgModule() providers option if a module is lazy loaded. The module's own injector is configured with the provider when that module is loaded, and Angular can inject the corresponding services in any class it creates in that module. If you use the @Injectable() option providedIn: MyLazyloadModule, the provider could be shaken out at compile time, if it is not used anywhere else in the app.

For both root-level and module-level injectors, a service instance lives for the life of the app or module, and Angular injects this one service instance in every class that needs it.

Component-level providers configure each component instance's own injector. Angular can only inject the corresponding services in that component instance or one of its descendant component instances. Angular can't inject the same service instance anywhere else.

A component-provided service may have a limited lifetime. Each new instance of the component gets its own instance of the service. When the component instance is destroyed, so is that service instance.

In our sample app, HeroComponent is created when the application starts and is never destroyed, so the HeroService instance created for HeroComponent lives for the life of the app. If you want to restrict HeroService access to HeroComponent and its nested HeroListComponent, provide HeroService at the component level, in HeroComponent metadata.

@Injectable-level configuration

The @Injectable() decorator identifies every service class. The providedIn metadata option for a service class configures a specific injector (typically root) to use the decorated class as a provider of the service. When an injectable class provides its own service to the root injector, the service is available anywhere the class is imported.

The following example configures a provider for HeroService using the @Injectable() decorator on the class.

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

@Injectable({
  providedIn: 'root',
})
export class HeroService {
  constructor() { }
}

This configuration tells Angular that the app's root injector is responsible for creating an instance of HeroService by invoking its constructor, and for making that instance available across the application.

Providing a service with the app's root injector is a typical case, and the CLI sets up this kind of a provider automatically for you when generating a new service. However, you might not always want to provide your service at the root level. You might, for instance, want users to explicitly opt-in to using the service.

Instead of specifying the root injector, you can set providedIn to a specific NgModule.

For example, in the following excerpt, the @Injectable() decorator configures a provider that is available in any injector that includes the HeroModule.

import { Injectable } from '@angular/core';
import { HeroModule } from './hero.module';
import { HEROES } from './mock-heroes';

@Injectable({
  // we declare that this service should be created
  // by any injector that includes HeroModule.
  providedIn: HeroModule,
})
export class HeroService {
  getHeroes() { return HEROES; }
}

This is generally no different from configuring the injector of the NgModule itself, except that the service is tree-shakable if the NgModule doesn't use it. It can be useful for a library that offers a particular service that some components might want to inject optionally, and leave it up to the app whether to provide the service.

@NgModule-level injectors

You can configure a provider at the module level using the providers metadata option for a non-root NgModule, in order to limit the scope of the provider to that module. This is the equivalent of specifying the non-root module in the @Injectable() metadata, except that the service provided via providers is not tree-shakable.

You generally don't need to specify AppModule with providedIn, because the app's root injector is the AppModule injector. However, if you configure a app-wide provider in the @NgModule() metadata for AppModule, it overrides one configured for root in the @Injectable() metadata. You can do this to configure a non-default provider of a service that is shared with multiple apps.

Here is an example of the case where the component router configuration includes a non-default location strategy by listing its provider in the providers list of the AppModule.

providers: [
  { provide: LocationStrategy, useClass: HashLocationStrategy }
]

@Component-level injectors

Individual components within an NgModule have their own injectors. You can limit the scope of a provider to a component and its children by configuring the provider at the component level using the @Component metadata.

The following example is a revised HeroesComponent that specifies HeroService in its providers array. HeroService can provide heroes to instances of this component, or to any child component instances.

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

import { HeroService } from './hero.service';

@Component({
  selector: 'app-heroes',
  providers: [ HeroService ],
  template: `
    <h2>Heroes</h2>
    <app-hero-list></app-hero-list>
  `
})
export class HeroesComponent { }

Element injectors

An injector does not actually belong to a component, but rather to the component instance's anchor element in the DOM. A different component instance on a different DOM element uses a different injector.

Components are a special type of directive, and the providers property of @Component() is inherited from @Directive(). Directives can also have dependencies, and you can configure providers in their @Directive() metadata. When you configure a provider for a component or directive using the providers property, that provider belongs to the injector for the anchor DOM element. Components and directives on the same element share an injector.

Injector bubbling

Consider this guide's variation on the Tour of Heroes application. At the top is the AppComponent which has some subcomponents, such as the HeroesListComponent. The HeroesListComponent holds and manages multiple instances of the HeroTaxReturnComponent. The following diagram represents the state of this three-level component tree when there are three instances of HeroTaxReturnComponent open simultaneously.

injector tree

When a component requests a dependency, Angular tries to satisfy that dependency with a provider registered in that component's own injector. If the component's injector lacks the provider, it passes the request up to its parent component's injector. If that injector can't satisfy the request, it passes the request along to the next parent injector up the tree. The requests keep bubbling up until Angular finds an injector that can handle the request or runs out of ancestor injectors. If it runs out of ancestors, Angular throws an error.

If you have registered a provider for the same DI token at different levels, the first one Angular encounters is the one it uses to provide the dependency. If, for example, a provider is registered locally in the component that needs a service, Angular doesn't look for another provider of the same service.

You can cap the bubbling by adding the @Host() parameter decorator on the dependant-service parameter in a component's constructor. The hunt for providers stops at the injector for the host element of the component.

If you only register providers with the root injector at the top level (typically the root AppModule), the tree of injectors appears to be flat. All requests bubble up to the root injector, whether you configured it with the bootstrapModule method, or registered all providers with root in their own services.

Component injectors

The ability to configure one or more providers at different levels opens up interesting and useful possibilities. The guide sample offers some scenarios where you might want to do so.

Scenario: service isolation

Architectural reasons may lead you to restrict access to a service to the application domain where it belongs. For example, the guide sample includes a VillainsListComponent that displays a list of villains. It gets those villains from a VillainsService.

If you provide VillainsService in the root AppModule (where you registered the HeroesService), that would make the VillainsService available everywhere in the application, including the Hero workflows. If you later modified the VillainsService, you could break something in a hero component somewhere. Providing the service in the root AppModule creates that risk.

Instead, you can provide the VillainsService in the providers metadata of the VillainsListComponent like this:

@Component({
  selector: 'app-villains-list',
  templateUrl: './villains-list.component.html',
  providers: [ VillainsService ]
})

By providing VillainsService in the VillainsListComponent metadata and nowhere else, the service becomes available only in the VillainsListComponent and its sub-component tree. It's still a singleton, but it's a singleton that exist solely in the villain domain.

Now you know that a hero component can't access it. You've reduced your exposure to error.

Scenario: multiple edit sessions

Many applications allow users to work on several open tasks at the same time. For example, in a tax preparation application, the preparer could be working on several tax returns, switching from one to the other throughout the day.

This guide demonstrates that scenario with an example in the Tour of Heroes theme. Imagine an outer HeroListComponent that displays a list of super heroes.

To open a hero's tax return, the preparer clicks on a hero name, which opens a component for editing that return. Each selected hero tax return opens in its own component and multiple returns can be open at the same time.

Each tax return component has the following characteristics:

  • Is its own tax return editing session.
  • Can change a tax return without affecting a return in another component.
  • Has the ability to save the changes to its tax return or cancel them.
Heroes in action

Suppose that the HeroTaxReturnComponent has logic to manage and restore changes. That would be a pretty easy task for a simple hero tax return. In the real world, with a rich tax return data model, the change management would be tricky. You could delegate that management to a helper service, as this example does.

Here is the HeroTaxReturnService. It caches a single HeroTaxReturn, tracks changes to that return, and can save or restore it. It also delegates to the application-wide singleton HeroService, which it gets by injection.

import { Injectable }    from '@angular/core';
import { HeroTaxReturn } from './hero';
import { HeroesService } from './heroes.service';

@Injectable()
export class HeroTaxReturnService {
  private currentTaxReturn: HeroTaxReturn;
  private originalTaxReturn: HeroTaxReturn;

  constructor(private heroService: HeroesService) { }

  set taxReturn (htr: HeroTaxReturn) {
    this.originalTaxReturn = htr;
    this.currentTaxReturn  = htr.clone();
  }

  get taxReturn (): HeroTaxReturn {
    return this.currentTaxReturn;
  }

  restoreTaxReturn() {
    this.taxReturn = this.originalTaxReturn;
  }

  saveTaxReturn() {
    this.taxReturn = this.currentTaxReturn;
    this.heroService.saveTaxReturn(this.currentTaxReturn).subscribe();
  }
}

Here is the HeroTaxReturnComponent that makes use of it.

import { Component, EventEmitter, Input, Output } from '@angular/core';
import { HeroTaxReturn }        from './hero';
import { HeroTaxReturnService } from './hero-tax-return.service';

@Component({
  selector: 'app-hero-tax-return',
  templateUrl: './hero-tax-return.component.html',
  styleUrls: [ './hero-tax-return.component.css' ],
  providers: [ HeroTaxReturnService ]
})
export class HeroTaxReturnComponent {
  message = '';

  @Output() close = new EventEmitter<void>();

  get taxReturn(): HeroTaxReturn {
    return this.heroTaxReturnService.taxReturn;
  }

  @Input()
  set taxReturn (htr: HeroTaxReturn) {
    this.heroTaxReturnService.taxReturn = htr;
  }

  constructor(private heroTaxReturnService: HeroTaxReturnService) { }

  onCanceled()  {
    this.flashMessage('Canceled');
    this.heroTaxReturnService.restoreTaxReturn();
  };

  onClose()  { this.close.emit(); };

  onSaved() {
    this.flashMessage('Saved');
    this.heroTaxReturnService.saveTaxReturn();
  }

  flashMessage(msg: string) {
    this.message = msg;
    setTimeout(() => this.message = '', 500);
  }
}

The tax-return-to-edit arrives via the input property which is implemented with getters and setters. The setter initializes the component's own instance of the HeroTaxReturnService with the incoming return. The getter always returns what that service says is the current state of the hero. The component also asks the service to save and restore this tax return.

This won't work if the service is an application-wide singleton. Every component would share the same service instance, and each component would overwrite the tax return that belonged to another hero.

To prevent this, we configure the component-level injector of HeroTaxReturnComponent to provide the service, using the providers property in the component metadata.

providers: [ HeroTaxReturnService ]

The HeroTaxReturnComponent has its own provider of the HeroTaxReturnService. Recall that every component instance has its own injector. Providing the service at the component level ensures that every instance of the component gets its own, private instance of the service, and no tax return gets overwritten.

The rest of the scenario code relies on other Angular features and techniques that you can learn about elsewhere in the documentation. You can review it and download it from the live example.

Scenario: specialized providers

Another reason to re-provide a service at another level is to substitute a more specialized implementation of that service, deeper in the component tree.

Consider a Car component that depends on several services. Suppose you configured the root injector (marked as A) with generic providers for CarService, EngineService and TiresService.

You create a car component (A) that displays a car constructed from these three generic services.

Then you create a child component (B) that defines its own, specialized providers for CarService and EngineService that have special capabilities suitable for whatever is going on in component (B).

Component (B) is the parent of another component (C) that defines its own, even more specialized provider for CarService.

car components

Behind the scenes, each component sets up its own injector with zero, one, or more providers defined for that component itself.

When you resolve an instance of Car at the deepest component (C), its injector produces an instance of Car resolved by injector (C) with an Engine resolved by injector (B) and Tires resolved by the root injector (A).

car injector tree

The code for this cars scenario is in the car.components.ts and car.services.ts files of the sample which you can review and download from the live example.

© 2010–2019 Google, Inc.
Licensed under the Creative Commons Attribution License 4.0.
https://v7.angular.io/guide/hierarchical-dependency-injection