Hierarchical Dependency Injection In Angular
In this article, We will learn about the how Angular dependency injection works. Angular creates a dependency injection framework that is hierarchical. It produces a tree of Injectors that is hierarchical. Angular Providers is duplicated for each Injector. The Angular dependency injection framework is built around these two components. The injector tree will be created by Angular. The injector's method for resolving the dependency.
Injector
For each Component, Directive, or other object loaded by Angular, an Injector instance is created. It also builds injector instances for the Root Module as well as each lazy loaded module. However, eagerly loaded modules share the Root Module's injector rather than having their own.
Injector Tree
Angular creates two injector trees instead of one. Element Injector Tree& Module Injector Tree
Modules (@NgModule) are injected through the Module Injector tree. For the Root Module and all Lazy Loaded Modules.
Components and Directives are DOM Elements, and the Element Injector tree is for them.
Module Injector Tree
For the services to be delivered at Module Levels, Angular creates the ModuleInjector.
The Module level services are registered in two ways.
- Using the @NgModule's Providers Metadata ()
- Using the @Injectable() Decorator in the service itself with providedIn: root
Angular produces a Null Injector instance at the top of the Module Injector hierarchy. Unless we use the Optional decorator to decorate the dependency, the Null Injector always raises an error.
Angular produces a PlatformInjector object under Null Injector. Platform Injector typically comes with built-in suppliers such as DomSanitize and others.
Angular generates the Root Module Injector under the Platform Injector. It's set up with providers from the places listed below.
@NgModule of Root Module metadata provider.
Provides @NgModule metadata for every imported Modules (i.e. all eagerly loaded modules).
All services with the value root or any in their @Injectable() decorator have providedIn metadata. It contains services from both eagerly and slowly loaded modules.
Angular creates an Injector object for each Lazy loaded Module under Root Module Injector. They are only created when Angular loads them. They're set up with suppliers from the places listed below.
Provides metadata for the @NgModule of the Lazy loaded Module.
All services that have a value of any in their @Injectable() decorator have providedIn metadata.
Element Injector Tree
For services to be offered at the element level, such as Components and Directives, Angular develops the Element Injector tree.
When the application is launched, Angular constructs the Element Injector tree.
The Root Component's Injector instance becomes the Element Injector tree's root Injector. It gets the Providers from the Root Component's provider property.
Every element (i.e. Components or Directives) we build has a parent, which is the Root Component. Each of the elements can have children, resulting in an element tree. For each of these elements, Angular creates an Injector, forming a tree of Injectors.
The list of Providers is obtained by each Injector via the @Directive() or @Component() methods. Angular produces an empty Injector if the Providers array is empty.
Dependency Resolution
The dependence is resolved in two stages by Angular.
- First, use the Element Injector and its parents to solve it.
- If the element isn't discovered in the Element Injector, check the Module Injector and its parents.
The token is used by the components to request dependencies in the constructor.
In the Element Injector tree, the search begins with the Injector connected with the component. It looks for dependencies in its Providers array using the token. Injector tries to see if the service instance already exists if it finds the provider. If the component already exists, it is injected into it; otherwise, a new instance is created. The component is then injected with it.
The request is passed to the parent by the injector. The Element's Injector
The root component owns the topmost Injector in the Element Injector tree. It does not throw an exception if the dependence is not discovered; instead, it returns.
The focus now switches to Module Injector Tree.
The Injector associated with the module to which the element belongs is where the search begins. The search for Root Modules and Eagerly Loaded Modules begins with the Root Module Injector. The resolutions for components from lazy loaded modules begin with the Module Injector associated with the Lazy Loaded Module.
The request will continue until it reaches Null Injector, which is the topmost injector in the Module Injector tree. There are no providers in the Null Injector. Its function is to throw the No provider for Service error. However, if we adorn the
Dependency Resolution in Picture
The graphic below illustrates how Angular resolves the dependency for a component in a Root Module.
The component requesting dependence (GrChildComponentA) is in the RootModule. The search begins with the Injector linked with the GrChildComponentA in the Element Injector hierarchy. The search progresses through the Injector tree until it reaches the Root Injector, which is part of the RootComponent. If the service cannot be located, the search moves to the Module Injector Hierarchy, starting with the Root Module (because GrChildComponentA belongs to Root Module).
In a lazy loaded module, the following picture displays how Angular resolves the dependency for component.
The LazyModuleB contains the component (LazyBChildComponent) that requests dependence. As you can see, when the Element Injector tree's Injectors fail to offer the requested service, the request is sent on to the Module Injector Hierarchy. The search should begin with the Module that LazyBChildComponent belongs to, which is LazyModuleB.
Notes on Dependency Resolution
Element Injector Tree is not a child of Module Injector Tree. In the Element Injector Tree, each Element can have the same parent, however in the Module Injector Tree, each Element can have a different parent.
Eager Modules have no injectors. They both have access to it, as does the Root Module.
Module Ejector Tree: Separate Injector for Lazy Loaded Modules
If two Eager Modules offer the same service for the same token, the module in the imports array that appears last wins. For example, providers of Eager2Module overwrite providers of Eager1Module for the same token in the following imports.
imports: [BrowserModule, FormsModule, Eager1Module, Eager2Module],
Example of hierarchical dependency injection
import { Injectable } from '@angular/core'; @Injectable() export class AppService { sharedValue: string; constructor() { console.log('Shared Service initialised'); this.sharedValue = 'App:' + Math.round(Math.random() * 100); console.log(this.sharedValue); } public getSharedValue() { return this.sharedValue; } }
All components inject and display the results of all services. The @Optional() decorator is used. If the dependency is not detected, the Injector will return null instead of an error.
import { Component, Optional } from '@angular/core'; import { AppService } from './app.service'; import { EagerService } from './eager/eager.service'; import { LazyService } from './lazy/lazy.service'; @Component({ selector: 'parent1-component', template: ` <div class="box"> <strong>Parent1</strong> {{ appValue }} {{ eagerValue }} {{ lazyValue }} <child1-component></child1-component> <child2-component></child2-component> </div> `, providers: [] }) export class Parent1Component { appValue; eagerValue; lazyValue; constructor( @Optional() private appService: AppService, @Optional() private eagerService: EagerService, @Optional() private lazyService: LazyService ) { this.appValue = appService?.getSharedValue(); this.eagerValue = eagerService?.getSharedValue(); this.lazyValue = lazyService?.getSharedValue(); } }
Provide Services In The Respective Modules
- Because they are available in the RootModule Injector, AppService and EagerService return the same value everywhere.
- Only the LazyModule Injector supports LazyService. As a result, it's only available in the LazyComponent.
- Instead of EagerModule, register EagerService in AppModule.
- Both EagerService and AppService should have providedIn: 'root'.
Using ProvidedIn in LazyService
- For the LazyService, add providedIn:'root'. Root Module Injector now includes LazyService.
- Remove LazyService from the Array of LazyModules in the Provider.
@Injectable({ providedIn: 'root' }) export class LazyService {
- For the LazyService, keep providedIn:'root'.
- Add LazyService to the LazyModule's Providers array.
LazyService in AppComponent
- For the LazyService, keep providedIn:'root'.
- Add LazyService to the LazyModule's Providers array.
- Add LazyService to the AppComponent's Providers array.