Skip to content

Instantly share code, notes, and snippets.

@kapunahelewong kapunahelewong/injectors.md Secret
Last active Feb 14, 2019

Embed
What would you like to do?
Injector trees

Injector trees

Types of Angular injectors

There are two kinds of Injectors in an Angular application:

  • ModuleInjector: An injector created as a result of @NgModule.
  • ElementInjector: An injector created as a result of component/directive on an element

Both injectors are hierarchical, which means that when an injector is resolving a value, if the value is not found, the injector asks its parent injector for resolution. The implication of this is that depending where one is in the injector tree, the same query for a token may return a different instance, or throw an error if not found.

ModuleInjector

A ModuleInjector is created from the @Injectible() providedIn property. Prior to Angular version 6, the the standard practice was to use the @NgModule providers array, however, since version 6, the @Injectible() providedIn property is preferable because it makes your services tree-shakable, which helps minimize the size of your app.

@NgModule is configured from the providers property of @NgModule. If @NgModule imports other @NgModules, all of those providers from imported @NgModules are flattened into a single ModuleInjector. Child ModuleInjectors are created by lazy loading other @NgModules, not by importing them.

ElementInjector

A component or directive creates an ElementInjector and is given a ModuleInjector. When resolving a token for a component/directive, the resolution happens in two phases:

  1. Resolve it against the Element Injector and its parents.
  2. Resolve it against the ModuleInjector and its parents.

NOTE: ModuleInjector is not a parent of ElementInjector. Each Element can in theory have a different ModuleInjector.

The ElementInjector tree

Angular creates the ElementInjector tree dependent upon the relationship of components or directives. Where those components or directives fall in the tree determines their availability. If they are imported in the root, they are available throughout the app. If, instead, an NgModule is imported in a lazy loaded feature module, that module is available to children of that feature module and is not available to the entire app.

The following diagram shows an ElementInjector tree that shows the relationship between a parent <my-app> node and its child nodes, <router-outlet>, <md-button>, and <tooltip>.

Element Injector Tree

The ModuleInjector tree

In an app with lazy loaded modules, the compiler builds a new section of the ModuleInjector tree and attaches it to the already established tree.

Here is the same app's ModuleInjector tree. In this example, there are two lazy loaded routes, LazyARoute and LazyBRoute. Within LazyARoute there is a lazy loaded child route called SubLazyARoute.

Module Injector Tree

Resolving injectibles

When an NgModule requests a service, Angular looks for it in the current context. For example, if the context is the <tooltip> for LazyBRoute and you need Foo, Angular can see Foo because the parent of the <tooltip>, <md-button> provides it.

If the parent node doesn't provide the service, however, Angular keeps looking. First, it checks in the ElementInjector tree starting with the node where the request originates. If the service isn't in there, Angular looks for it in the parent ElementInjector and continues up the tree until it finds it or until it reaches the top.

If Angular doesn't find the service at the top of the ElementInjector tree, it makes a second pass through the ModuleInjector tree. First, it finds the ModuleInjector on the node where the query originated and if it isn't there, it looks to the parent. If the service isn't there, Angular continues up the tree checking to the top if it doesn't find the service in any child node.

In brief, the search process is as follows:

  • Pass 1: Angular looks at the ElementInjector and its parents. If not found, Angular goes to Pass 2.
  • Pass 2: On the node where query initiated, get the ModuleInjector and look it and its parents.

For example, if the context were SubLazyBRoute <md-button>, and <md-button> requested ShoppingCart, first, Angular looks for ShoppingCart in the ElementInjector tree, starting at the ElementInjector for <md-button> (1). Since it's not there, it looks in its parent, MyAppModuleInjector (2) but ShoppingCart isn't there, either, so the search continues to the top of the tree (3).

ElementInjector Flow

Since ShoppingCart is nowhere in the ElementInjector tree, Angular then looks to the ModuleInjector tree. First, it finds the ModuleInjector node where <md-button> resides, which is LazyBRoute (4). Angular finds ShoppingCart in the ModuleInjector for LazyBRoute.

Injector Flow

If <md-button> were looking instead for AddItem, it wouldn't be available because AddItem belongs to SubLazyBRoute, and the parent, LazyBRoute wouldn't know about it. Likewise, if <md-button> requested Baz, which is in LazyARoute, it would not gain access to it. This is because LazyARoute is a sibling of LazyBRoute, where <md-button> resides, and Angular traverses the trees in one direction—and does not make leaps from sibling to sibling.

Resolution modifiers

like @Self, @SkipSelf and @Optional @Host()

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.

  • See an example of using @Host together with @Optional, another parameter decorator that lets you handle the null case if no provider is found.

Configuring injectors

Conclusion/Implications


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.