Hierarchical injectors

Domains: Angular

Injectors in Angular have rules that you can leverage to achieve the desired visibility of injectables in your apps. By understanding these rules, you can determine in which NgModule, Component or Directive you should declare a provider.

Two injector hierarchies

There are two injector hierarchies in Angular:

  1. ModuleInjector hierarchy—configure a ModuleInjector in this hierarchy using an @NgModule() or @Injectable() annotation.
  2. ElementInjector hierarchy—created implicitly at each DOM element. An ElementInjector is empty by default unless you configure it in the providers property on @Directive() or @Component().

ModuleInjector

The ModuleInjector can be configured in one of two ways:

  • Using the @Injectable() providedIn property to refer to @NgModule(), or root.
  • Using the @NgModule() providers array.

Tree-shaking and @Injectable()

Using the @Injectable() providedIn property is preferable to the @NgModule() providers array because with @Injectable() providedIn, optimization tools can perform tree-shaking, which removes services that your app isn't using and results in smaller bundle sizes.

Tree-shaking is especially useful for a library because the application which uses the library may not have a need to inject it. Read more about tree-shakable providers in DI Providers.

ModuleInjector is configured by the @NgModule.providers and NgModule.imports property. ModuleInjector is a flattening of all of the providers arrays which can be reached by following the NgModule.imports recursively.

Child ModuleInjectors are created when lazy loading other @NgModules.

Provide services with the providedIn property of @Injectable() as follows:

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

@Injectable({
  providedIn: 'root'  // <--provides this service in the root ModuleInjector
})
export class ItemService {
  name = 'telephone';
}

The @Injectable() decorator identifies a service class. The providedIn property configures a specific ModuleInjector, here root, which makes the service available in the root ModuleInjector.

Platform injector

There are two more injectors above root, an additional ModuleInjector and NullInjector().

Consider how Angular bootstraps the app with the following in main.ts:

platformBrowserDynamic().bootstrapModule(AppModule).then(ref => {...})

The bootstrapModule() method creates a child injector of the platform injector which is configured by the AppModule. This is the root ModuleInjector.

The platformBrowserDynamic() method creates an injector configured by a PlatformModule, which contains platform-specific dependencies. 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. You can configure additional platform-specific providers at the platform level by supplying extraProviders using the platformBrowser() function.

The next parent injector in the hierarchy is the NullInjector(), which is the top of the tree. If you've gone so far up the tree that you are looking for a service in the NullInjector(), you'll get an error unless you've used @Optional() because ultimately, everything ends at the NullInjector() and it returns an error or, in the case of @Optional()null. For more information on @Optional(), see the @Optional() section of this guide.

The following diagram represents the relationship between the root ModuleInjector and its parent injectors as the previous paragraphs describe.

  NullInjector, ModuleInjector, root injector  

While the name root is a special alias, other ModuleInjectors don't have aliases. You have the option to create ModuleInjectors whenever a dynamically loaded component is created, such as with the Router, which will create child ModuleInjectors.

All requests forward up to the root injector, whether you configured it with the bootstrapModule() method, or registered all providers with root in their own services.

@Injectable() vs. @NgModule()

If you configure an app-wide provider in the @NgModule() of 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 }
]

ElementInjector

Angular creates ElementInjectors implicitly for each DOM element.

Providing a service in the @Component() decorator using its providers or viewProviders property configures an ElementInjector. For example, the following TestComponent configures the ElementInjector by providing the service as follows:

@Component({
  ...
  providers: [{ provide: ItemService, useValue: { name: 'lamp' } }]
})
export class TestComponent

Note: Please see the resolution rules section to understand the relationship between the ModuleInjector tree and the ElementInjector tree.

When you provide services in a component, that service is available via the ElementInjector at that component instance. It may also be visible at child component/directives based on visibility rules described in the resolution rules section.

When the component instance is destroyed, so is that service instance.

@Directive() and @Component()

A component is a special type of directive, which means that just as @Directive() has a providers property, @Component() does too. This means that directives as well as components can configure providers, using the providers property. When you configure a provider for a component or directive using the providers property, that provider belongs to the ElementInjector of that component or directive. Components and directives on the same element share an injector.

Resolution rules

When resolving a token for a component/directive, Angular resolves it in two phases:

  1. Against the ElementInjector hierarchy (its parents)
  2. Against the ModuleInjector hierarchy (its parents)

When a component declares a dependency, Angular tries to satisfy that dependency with its own ElementInjector. If the component's injector lacks the provider, it passes the request up to its parent component's ElementInjector.

The requests keep forwarding up until Angular finds an injector that can handle the request or runs out of ancestor ElementInjectors.

If Angular doesn't find the provider in any ElementInjectors, it goes back to the element where the request originated and looks in the ModuleInjector hierarchy. If Angular still doesn't find the provider, it 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 resolve 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.

Resolution modifiers

Angular's resolution behavior can be modified with @Optional()@Self()@SkipSelf() and @Host(). Import each of them from @angular/core and use each in the component class constructor when you inject your service.

For a working app showcasing the resolution modifiers that this section covers, see the resolution modifiers example / download example.

Types of modifiers

Resolution modifiers fall into three categories:

  1. What to do if Angular doesn't find what you're looking for, that is @Optional()
  2. Where to start looking, that is @SkipSelf()
  3. Where to stop looking, @Host() and @Self()

By default, Angular always starts at the current Injector and keeps searching all the way up. Modifiers allow you to change the starting (self) or ending location.

Additionally, you can combine all of the modifiers except @Host() and @Self() and of course @SkipSelf() and @Self().

@Optional()

@Optional() allows Angular to consider a service you inject to be optional. This way, if it can't be resolved at runtime, Angular simply resolves the service as null, rather than throwing an error. In the following example, the serviceOptionalService, isn't provided in the service@NgModule(), or component class, so it isn't available anywhere in the app.

export class OptionalComponent {
  constructor(@Optional() public optional?: OptionalService) {}
}

@Self()

Use @Self() so that Angular will only look at the ElementInjector for the current component or directive.

A good use case for @Self() is to inject a service but only if it is available on the current host element. To avoid errors in this situation, combine @Self() with @Optional().

For example, in the following SelfComponent, notice the injected LeafService in the constructor.

@Component({
  selector: 'app-self-no-data',
  templateUrl: './self-no-data.component.html',
  styleUrls: ['./self-no-data.component.css']
})
export class SelfNoDataComponent {
  constructor(@Self() @Optional() public leaf?: LeafService) { }
}

In this example, there is a parent provider and injecting the service will return the value, however, injecting the service with @Self() and @Optional() will return null because @Self() tells the injector to stop searching in the current host element.

Another example shows the component class with a provider for FlowerService. In this case, the injector looks no further than the current ElementInjector because it finds the FlowerService and returns the yellow flower

@SkipSelf()

@SkipSelf() is the opposite of @Self(). With @SkipSelf(), Angular starts its search for a service in the parent ElementInjector, rather than in the current one. So if the parent ElementInjector were using the value 

Similar pages

Page structure
Terms

Component

Angular

Service

Provider

Injectable

NgModule

AppModule

Router