Angular
Enterprise
./assets/drawing/angular-courses-dependency-injection.svg

Dependency injection in Angular

Dependency injection (DI) is a design pattern in which a class asks for dependencies from external sources (the injectors) rather than creating them itself. Angular has its own DI framework that helps to write modular applications. The dependencies are nothing more than services or objects with a clear lifecycle depending on their configuration.

Injectors are inherited, which means that if a given injector can't resolve a dependency, it asks the parent injector. A component/directives can get services from its own injector, from the injectors of its component ancestors, from the injector of its parent NgModule, or from the root injector (created from the app module).

Organizing dependencies

  • Tree-shaking services are possible from Angular version six by adding directly in the service the elements providedIn: 'root', 'platform', 'any', like that if the service is never injected it will be removed from the bundle at compilation.
  • Core services (not lazy) must be in the core folder and can declared either in the providers: [ ] array of the core module or better by using the providedIn: 'root' syntax from their injectable decorators and then can be used in all kind of modules (lazy or not) without putting them in providers: [ ] array of any module.
  • Shared services (shared by multiple lazy or core modules) if you put your services inside the provider array of the shared module and then use shared like its intented to use in multiple lazy modules then every lazy loaded module would get its own service instance and not the intented singleton, indeed this is due to the fact that lazy-loaded modules have their own injectors. To solve this issue you can use the ModuleWithProviders interface and create two methods: forRoot/forChild with providers that are going to be imported into both eager and lazy module modules or simply in several lazy modules. This solution is used by the Angular framework itself to solve the issue of the Route service in the Router module.
  • Feature services (lazy module) can be scoped to that feature by removing the providedIn: 'root' from their injectable decorators and adding them to the providers: [ ] array of the lazy feature module instead.
  • Component services can be scoped to that component by removing the providedIn: 'root' from their injectable decorators and adding them to the providers: [ ] array of the component. The service will be available in all child components, the view child and the content child. In addition to providers you can add viewProviders array if you want to scope the same token (with different class) only for the component view itself and consequently the content children (ng-content) will use the service from the providers array defined first.
  • Shared services between multiple apps or Angular Elements. You can use the providedIn: 'platform' in order to make a service available between multiple apps or Angular Elements.
  • Non singleton services can be created using the providedIn: 'any' in order to create isolated (contrary to a singleton) services for every child injector.

Configuring dependencies

  • Then you have to understand the different injection configuration that you can do, in fact you can configure the injection with different types of objects, either a class, an object or a simple value, a factory and even more. You will be required to use the InjectionToken mechanism if the type has no runtime representation, for example an interface otherwise you can directly pass your without InjectionToken.
  • class: { provide: MyService, useClass: MyService } // It is a also possible to use a shortcut: MyService.
  • value: { provide: 'MY_CONST', useValue: 'https://angular.io } // 'MY_CONST' can be declared as a String without InjectionToken.
  • value: { provide: MY_CONST, useValue: https://angular.io } // MY_CONST can be declared as InjectionToken.
  • value: { provide: MyInterface, useValue: { value: 'https://angular.io} } // MyInterface must be declared as InjectionToken.
  • factory: { provide: MyObs, deps: [DOCUMENT], useFactory: doThingFactory } // MyObs must be declared as InjectionToken<Observable> and doThingFactory is a function which return the observable. You can also create your factory using the 2ng argument of InjectionToken. Take care to understand the different between both: With useFactory it is not Tree-shakable, you have to declare the provider manually and you can easily switch between different implementation through a direrent useFactory function. With factory from InjectionToken it is Tree-shakable, the token is automatically provided in root but you can still change the implementation using useExisting function in the provider array.
  • existing: { provide: MyInterface, useExisting: forwardRef(() => MyDirective) } // MyDirective implements MyInterface and so forwardRef returns a directive after its instance is created. In general forwardRef is used when the injection is declared before the definition of the service or also sometimes when there is a circular dependency it helps to break it easily.

Decoratings dependencies

Those decorators below can be used to configure more precisely the injection behavior. They can be used in the constructor method or also in the deps array while providing a factory.

  • default: inject without any decorator, looking up the injectors hierarchy...
  • self: inject using only the provider from the component itself (@Self())
  • skipSelf: inject by skipping the provider from the component itself (@SkipSelf())
  • optional: inject if is provided else return null (@Optional())
  • host: inject looking in the component itself first and if is not found there, it looks for the injector up to its host component. (@Host()). Please note that there are special cases with directives and content projection.

Learn more about DI in Angular

Learn more about the dependency injection from the official Angular documentation

Learn more about Angular

Learn reactive programming with rxjs .