Hierarchical injectors
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:
-
ModuleInjector
hierarchy—configure aModuleInjector
in this hierarchy using an@NgModule()
or@Injectable()
annotation. -
ElementInjector
hierarchy—created implicitly at each DOM element. AnElementInjector
is empty by default unless you configure it in theproviders
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()
, orroot
. -
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 ModuleInjector
s 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.
While the name root
is a special alias, other ModuleInjector
s don't have aliases. You have the option to create ModuleInjector
s whenever a dynamically loaded component is created, such as with the Router, which will create child ModuleInjector
s.
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 ElementInjector
s 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 theElementInjector
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:
-
Against the
ElementInjector
hierarchy (its parents) -
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 ElementInjector
s.
If Angular doesn't find the provider in any ElementInjector
s, 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
Types of modifiers
Resolution modifiers fall into three categories:
-
What to do if Angular doesn't find what you're looking for, that is
@Optional()
-
Where to start looking, that is
@SkipSelf()
-
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 service, OptionalService
, 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