The Injector Hierarchy

Most Angular developers are aware that the framework establishes a root injector responsible for singleton providers. However, there is actually an injector positioned even higher than this one in the hierarchy.

From a developer's perspective, it's essential to grasp how Angular assembles its injector tree. Here is a representation of the uppermost portion of that tree:

What you always wanted to know about Angular Dependency Injection tree — figure 1

Uppermost Section of the Angular Injector Tree

This diagram is incomplete—no components are depicted yet. We'll expand on that later. For now, let's focus on the AppModule Injector, which is the most frequently utilized part of Angular's dependency injection system.

Root AppModule Injector

The well-documented Angular application root injector appears as AppModule Injector in the diagram above. As mentioned previously, this injector aggregates all providers from the transitive module graph. The practical consequence is:

When a module declares providers and is imported either directly in AppModule or transitively through any module already part of the AppModule import chain, those providers become visible application-wide.

By this logic, MyService2 originating from EagerModule2 ends up in the root injector.

Angular also attaches a ComponentFactoryResolver to the root module injector. This resolver is essential for dynamically creating components, holding factories for all entryComponents.

Another detail worth observing: the root injector also contains Module Tokens, which represent the types of every merged NgModule. We will revisit these tokens when discussing tree-shakeable providers.

To instantiate the NgModule injector, Angular runs the AppModule factory found in the module.ngfactory.js file.

What you always wanted to know about Angular Dependency Injection tree — figure 2

AppModule factory

This factory yields the module definition along with all aggregated providers, a familiar sight for many developers.

Tip: For a running Angular app in dev mode, you can inspect all providers on the root AppModule injector by entering this in the devtools console:

ng.probe(getAllAngularRootElements()[0]).injector.view.root.ngModule._providers

What you always wanted to know about Angular Dependency Injection tree — figure 3

Many other aspects of the root injector are thoroughly covered in the official documentation, so I will not repeat them here:

Platform Injector

The AppModule root injector is not the top of the tree; it has a parent known as NgZoneInjector, which itself is a child of PlatformInjector.

The platform injector typically holds built-in providers, but you can add your own during platform creation:

const platform = platformBrowserDynamic([ { 
  provide: SharedService, 
  deps:[] 
}]);
platform.bootstrapModule(AppModule);
platform.bootstrapModule(AppModule2);

Any extra providers supplied to the platform must be of type StaticProviders. If the distinction between StaticProvider and Provider is unclear, this SO answer clarifies it.

Tip: To see all providers from the Platform injector in a dev-mode application, run this in the devtools console:

ng.probe(getAllAngularRootElements()[0]).injector.view.root.ngModule._parent.parent._records;

// to see stringified value use
ng.probe(getAllAngularRootElements()[0]).injector.view.root.ngModule._parent.parent.toString()

What you always wanted to know about Angular Dependency Injection tree — figure 4

While the dependency resolution process at the AppModule injector level and beyond is fairly straightforward, I found the behavior at the component level quite puzzling. This prompted my investigation.

EntryComponent and RootData

When mentioning ComponentFactoryResolver, I referred to entryComponents. These components are typically specified in either the bootstrap or entryComponents array of an NgModule. The Angular router also generates components dynamically.

For each entryComponent, Angular creates a host factory, making these the root views for all other views. Consequently:

When any dynamic component is created, Angular establishes a root view seeded with root data, holding references to the elInjector and the ngModule injector.

function createRootData(
    elInjector: Injector, ngModule: NgModuleRef<any>, rendererFactory: RendererFactory2,
    projectableNodes: any[][], rootSelectorOrNode: any): RootData {
  const sanitizer = ngModule.injector.get(Sanitizer);
  const errorHandler = ngModule.injector.get(ErrorHandler);
  const renderer = rendererFactory.createRenderer(null, null);
  return {
    ngModule,
    injector: elInjector, projectableNodes,
    selectorOrNode: rootSelectorOrNode, sanitizer, rendererFactory, renderer, errorHandler
  };
}

Let's imagine an Angular application is running.

What unfolds when this piece of code executes?

platformBrowserDynamic().bootstrapModule(AppModule);

Countless operations happen in the background, but our focus is on the moment Angular creates the entry component.

const compRef = componentFactory.create(Injector.NULL, [], selectorOrNode, ngModule);

This is the pivotal point where the injector tree splits into two parallel branches.

Element

Injector vs Module Injector

With the widespread adoption of lazy loaded modules, a peculiar issue was reported on GitHub: the dependency injection system was instantiating lazy modules twice. This led to a redesign. Since that change, we have two separate trees: one for elements and one for modules.

The core principle is:

When a component or directive requests a dependency, Angular utilizes a Merge Injector to traverse the element injector tree. If the dependency is not located there, it switches to the module injector tree for resolution.

Note that I use "element injector" deliberately, not "component injector".

What exactly is the Merge Injector?

Have you ever written something like this?

@Directive({
  selector: '[someDir]'
}
export class SomeDirective {
 constructor(private injector: Injector) {}
}

In this scenario, the injected injector is a merge injector, which can similarly be injected into a component constructor.

The merge injector is defined as follows:

class Injector_ implements Injector {
  constructor(private view: ViewData, private elDef: NodeDef|null) {}
  get(token: any, notFoundValue: any = Injector.THROW_IF_NOT_FOUND): any {
    const allowPrivateServices =
        this.elDef ? (this.elDef.flags & NodeFlags.ComponentView) !== 0 : false;
    return Services.resolveDep(
        this.view, this.elDef, allowPrivateServices,
        {flags: DepFlags.None, token, tokenKey: tokenKey(token)}, notFoundValue);
  }
}

As the code shows, the Merge injector is simply a composition of the view and the element definition. It acts as a conduit between the element injector tree and the module injector tree during dependency resolution.

This injector can also provide access to built-in services like ElementRef, ViewContainerRef, TemplateRef, and ChangeDetectorRef. Intriguingly, it can even return another merge injector.

In fact, every element can have a merge injector, even if no tokens are provided on it.

Tip: To get a merge injector, enter this in the console:

ng.probe($0).injector

What you always wanted to know about Angular Dependency Injection tree — figure 5

So, what then is the element injector?

As is well known, Angular parses a template to generate a factory containing a view definition. The view is a representation of the template, composed of various node types like directive, text, provider, and query. Among these is the element node, which hosts the element injector. Angular stores provider information on this node using these properties:

export interface ElementDef {
  ...
  /**
   * visible public providers for DI in the view,
   * as see from this element. This does not include private providers.
   */
  publicProviders: {[tokenKey: string]: NodeDef}|null;
  /**
   * same as visiblePublicProviders, but also includes private providers
   * that are located on this element.
   */
  allProviders: {[tokenKey: string]: NodeDef}|null;
}

Let's examine how the element injector resolves a dependency:

const providerDef =
  (allowPrivateServices ? elDef.element!.allProviders :
    elDef.element!.publicProviders)![tokenKey];
if (providerDef) {
  let providerData = asProviderData(searchView, providerDef.nodeIndex);
  if (!providerData) {
    providerData = { instance: _createProviderInstance(searchView, providerDef) };
    searchView.nodes[providerDef.nodeIndex] = providerData as any;
  }
  return providerData.instance;
}

It simply consults the allProviders or publicProviders property, depending on the privacy of the lookup.

This injector holds the component or directive instance along with all providers registered by them.

These providers are populated during view instantiation, with the primary source being ProviderElementContext, a part of the Angular compiler. Diving into this class reveals some interesting nuances.

For instance, Angular enforces certain restrictions when using the Host decorator, and viewProviders on the host element can be a solution. (Also see this related discussion: https://medium.com/@a.yurich.zuev/angular-nested-template-driven-form-4a3de2042475).

Another interesting scenario: if a component and a directive on the same element both provide the same token, the directive's provider takes precedence.

Tip: To access the element injector, enter this in the console:

ng.probe($0).injector.elDef.element

What you always wanted to know about Angular Dependency Injection tree — figure 6

Resolution algorithm

The core of Angular's dependency resolution within a view is detailed here. This is the logic the merge injector uses in its get method, specifically via Services.resolveDep. To grasp this algorithm, you need to understand the concepts of a view and the view parent element.

Consider a root AppComponent with the template <child></child>. This creates three distinct views:

HostView_AppComponent
    <my-app></my-app>
View_AppComponent
    <child></child>
View_ChildComponent
    some content

The resolution algorithm navigates based on the view hierarchy:

What you always wanted to know about Angular Dependency Injection tree — figure 7

When a dependency is requested in a child component, the search begins at the child's element injector, checking its elRef.element.allProviders|publicProviders. If not found, it then ascends through all parent view elements(1), checking providers in each element injector. When the next parent view element is null(2), it returns to startView(3), checks startView.rootData.elnjector(4), and finally, if still unresolved, checks startView.rootData module.injector(5).

In essence, Angular looks for the parent element of the view, not the parent element of the component as it walks up the hierarchy. The function below is used to find the view parent element:

/**
 * for component views, this is the host element.
 * for embedded views, this is the index of the parent node
 * that contains the view container.
 */
export function viewParentEl(view: ViewData): NodeDef|null {
  const parentView = view.parent;
  if (parentView) {
    return view.parentNodeDef !.parent;
  } else {
    return null;
  }
}

Let's test this with a small example:

@Component({
  selector: 'my-app',
  template: `<my-list></my-list>`
})
export class AppComponent {}

@Component({
  selector: 'my-list',
  template: `
    <div class="container">
      <grid-list>
        <grid-tile>1</grid-tile>
        <grid-tile>2</grid-tile>
        <grid-tile>3</grid-tile>
      </grid-list>
    </div>
  `
})
export class MyListComponent {}

@Component({
  selector: 'grid-list',
  template: `<ng-content></ng-content>`
})
export class GridListComponent {}

@Component({
  selector: 'grid-tile',
  template: `...`
})
export class GridTileComponent {
  constructor(private gridList: GridListComponent) {}
}

Suppose we are inside a grid-tile component and attempt to inject GridListComponent. It resolves successfully, but how?

What is the view parent element at this moment?

Here's a step-by-step approach to figure it out:

  1. Locate the starting element. The GridTileComponent matches the grid-tile selector, so we find the grid-tile element.
  2. Identify the template that the grid-tile element belongs to, which is the MyListComponent template.
  3. Determine the view for this element. If it's not inside an embedded view, it's a component view (our case, so it's View_MyListComponent).
  4. Find the view parent element, which is the parent element for the view, not the element itself.

There are two possibilities here:

  • For an embedded view, this is the parent node that holds the view container.

For example, if we put a structural directive on grid-list:

@Component({
  selector: 'my-list',
  template: `
    <div class="container">
      <grid-list *ngIf="1">
        <grid-tile>1</grid-tile>
        <grid-tile>2</grid-tile>
        <grid-tile>3</grid-tile>
      </grid-list>
    </div>
  `
})
export class MyListComponent {}

Then the view parent element for grid-tile would be div.container.

  • For a component view, this is the host element.

In our original app, the view parent element is my-list, not grid-list.

So how does Angular resolve ****GridListComponent**** if it skips ****grid-list**** ?

The secret lies in how Angular gathers providers for elements: it employs prototypical inheritance.

Whenever a token is provided on an element, Angular creates new allProviders and publicProviders arrays that are inherited from the parent node. If no token is provided, the element simply shares the same arrays with its parent.

The implication is that grid-tile is already aware of all providers registered on any parent element within its view.

Here is the basic process Angular follows to gather providers for elements in a template:

What you always wanted to know about Angular Dependency Injection tree — figure 8

As shown, grid-tile can easily get GridListComponent from its element injector via allProviders, because those providers include ones from the parent element.

What you always wanted to know about Angular Dependency Injection tree — figure 9

Let's look at this SO answer for further details.

This prototypal provider pattern is why we run into issues using the multi option to provide tokens at multiple levels. Fortunately, DI is flexible, and there are workarounds, as shown here: https://stackoverflow.com/questions/49406615/is-there-a-way-how-to-use-angular-multi-providers-from-all-multiple-levels

With this foundation, we can proceed to graphically map the injector tree.

Simple my-app->child->grand-child application

Consider this straightforward application:

@Component({
  selector: 'my-app',
  template: `<child></child>`,
})
export class AppComponent {}

@Component({
  selector: 'child',
  template: `<grand-child></grand-child>`
})
export class ChildComponent {}

@Component({
  selector: 'grand-child',
  template: `grand-child`
})
export class GrandChildComponent {
  constructor(private service: Service) {}
}

@NgModule({
  imports: [BrowserModule],
  declarations: [
    AppComponent, 
    ChildComponent, 
    GrandChildComponent
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

We have three component levels, and Service is requested in GrandChildComponent.

my-app
   child
      grand-child(ask for Service dependency)

Here's the resolution path Angular takes.

What you always wanted to know about Angular Dependency Injection tree — figure 10

In the diagram, we begin at the grand-child element, found on View_Child (1). Angular ascends through all view parent elements. Once there are no more parent elements (since my-app has none), it first [looks at the root](https://github.com/angular/angular/blob/master/packages/core/src/view/provider.ts#L401) [elInjector] (2):

startView.root.injector.get(depDef.token, NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR);

Here, startView.root.injector is a NullInjector, which holds no tokens. Failing that, it switches to the module injector (3):

startView.root.ngModule.injector.get(depDef.token, notFoundValue);

Angular then proceeds to resolve the dependency in this manner:

AppModule Injector 
        ||
        \/
    ZoneInjector 
        ||
        \/
  Platform Injector 
        ||
        \/
    NullInjector 
        ||
        \/
       Error

Simple routed application

Let's add a router outlet to ChildComponent in our app.

@Component({
  selector: 'my-app',
  template: `<router-outlet></router-outlet>`,
})
export class AppComponent {}
...
@NgModule({
  imports: [
    BrowserModule,
    RouterModule.forRoot([
      { path: 'child', component: ChildComponent },
      { path: '', redirectTo: '/child', pathMatch: 'full' }
    ])
  ],
  declarations: [
    AppComponent,
    ChildComponent,
    GrandChildComponent
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

This changes the structure to resemble:

my-app
router-outlet
child
grand-child(dynamic creation

Now, let's examine the point where the router creates dynamic components:

const injector = new OutletInjector(activatedRoute, childContexts, this.location.injector);                           
this.activated = this.location.createComponent(factory, this.location.length, injector);

At this juncture, Angular creates a new root view with a fresh rootData. Notice that an OutletInjector is passed as the root elInjector. This OutletInjector is constructed with its parent as this.location.injector, which is the injector for the router-outlet element.

The OutletInjector is a special injector that acts as a link between the routed component and the parent router-outlet element. You can find its code here.

What you always wanted to know about Angular Dependency Injection tree — figure 11

Simple application with lazy loading

Finally, let's move GrandChildComponent into a lazy loaded module. This requires adding a router-outlet to the child component's view and adjusting the router configuration:

@Component({
  selector: 'child',
  template: `
    Child
    <router-outlet></router-outlet>
  `
})
export class ChildComponent {}
...
@NgModule({
  imports: [
    BrowserModule,
    RouterModule.forRoot([
      {
        path: 'child', component: ChildComponent,
        children: [
          { 
             path: 'grand-child', 
             loadChildren: './grand-child/grand-child.module#GrandChildModule'}
        ]
      },
      { path: '', redirectTo: '/child', pathMatch: 'full' }
    ])
  ],
  declarations: [
    AppComponent,
    ChildComponent
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}
my-app
   router-outlet
   child (dynamic creation)
       router-outlet
         +grand-child(lazy loading)

Let's draw the two separate trees for this lazy-loaded application:

What you always wanted to know about Angular Dependency Injection tree — figure 12

Tree-shakeable tokens are approaching

Angular is actively working on reducing the framework's footprint, and from version 6 onward it will offer an alternative mechanism for provider registration.

Injectable

Previously, applying the Injectable decorator to a class did not signal that it might have dependencies, nor was it tied to its usage elsewhere. In fact, if a service has no dependencies, the @Injectable() decorator can be omitted without any negative consequences.

Once the API becomes stable, we will be able to configure the Injectable decorator to inform Angular which module the service belongs to and how it should be instantiated:

export interface InjectableDecorator {
  (): any;
  (options?: {providedIn: Type<any>| 'root' | null}&InjectableProvider): any;
  new (): Injectable;
  new (options?: {providedIn: Type<any>| 'root' | null}&InjectableProvider): Injectable;
}

export type InjectableProvider = ValueSansProvider | ExistingSansProvider |
StaticClassSansProvider | ConstructorSansProvider | FactorySansProvider | ClassSansProvider;

A straightforward demonstration of this feature looks like this:

@Injectable({
  providedIn: 'root'
})
export class SomeService {}

@Injectable({
  providedIn: 'root',
  useClass: MyService,
  deps: []
})
export class AnotherService {}

With this approach, rather than including all providers in the NgModule factory, Angular saves the provider information inside the Injectable metadata. This is the key to making libraries smaller. If providers are registered via Injectable and consumers never import them, they won't appear in the final bundle. Therefore,

Favor registering providers in Injectable over NgModule.providers over Component.providers

Earlier we touched upon Module Tokens, which get attached to the root module injector. This allows Angular to identify which modules exist within a given module injector.

The Resolver leverages this data to verify whether a tree-shakeable token belongs to the module injector.

InjectionToken

Similarly, when using InjectionToken, we'll have the ability to specify how the DI system should construct the token and in which injectors it should be accessible.

export class InjectionToken<T> {
  constructor(protected _desc: string, options?: {
    providedIn?: Type<any>| 'root' | null,
    factory: () => T
  }) {}
}

This means it is intended to be utilized in the following manner:

export const apiUrl = new InjectionToken('tree-shakeable apiUrl token', {                                   
  providedIn: 'root',                               
  factory: () => 'someUrl'
});

Conclusion

Dependency injection is a deep and intricate subject within Angular. A clear grasp of its internals brings confidence to your daily work, so I highly recommend exploring the Angular source code from time to time…