Angular EnterpriseAngular Enterprise

Dependency injection in Angular

  • Courses
  • 20 practices
  • 6 min read
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 or directive can get services from its own injector, from the injectors of its ancestor components, from the injector of its parent route or NgModule, or from the root injector (created by bootstrapApplication or the app module).

Dependencies can be requested through the constructor parameters or with the inject() function, available since Angular 14 and now the recommended approach in modern Angular code.

Organizing dependencies

  • Tree-shakable services are possible since Angular version 6 by adding providedIn: 'root' (or 'platform') directly in the @Injectable() decorator of the service: 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 be declared either in the providers: [] array of bootstrapApplication (or of the core module) or by using the providedIn: 'root' syntax in their @Injectable() decorator, and then they can be used everywhere (lazy or not) without putting them in the providers: [] array of any module.
  • Shared services (shared by multiple lazy or core modules): if you put your services inside the providers array of the shared module and then import the shared module, as intended, in multiple lazy modules, then every lazy-loaded module gets its own service instance and not the intended singleton. This is due to the fact that lazy-loaded modules have their own injectors. The simplest solution is providedIn: 'root'. Otherwise you can use the ModuleWithProviders interface and create two static methods, forRoot()/forChild(), so that the providers are registered only once while the module is imported into both eager and lazy modules. This solution is used by the Angular framework itself for the Router service of the RouterModule.
  • Feature services (lazy route or module) can be scoped to that feature by removing the providedIn: 'root' from their @Injectable() decorator and adding them to the providers: [] array of the lazy route or of the lazy feature module instead. In this way the lazy loading of the service is explicitly done with the lazy feature, but be aware that with providedIn: 'root' the Angular compiler will also do this if and only if your service is used only in this lazy feature.
  • Component services can be scoped to that component by removing the providedIn: 'root' from their @Injectable() decorator and adding them to the providers: [] array of the component. The service will be available in all child components, the view children and the content children. In addition to providers you can add a viewProviders array if you want to scope the same token (with a different class) only to the component view itself, consequently the content children (ng-content) will use the service from the providers array defined first.
  • Platform services shared between multiple apps or Angular Elements. You can use providedIn: 'platform' in order to make a service available between multiple apps or Angular Elements.
  • Singleton services can be created using providedIn: 'root', this way the service will be available application wide as a singleton with no need to add it to a module's providers array as was the case on versions of Angular lower than v6.
  • Non singleton services could be created using providedIn: 'any' in order to create isolated (contrary to a singleton) services for every lazy-loaded injector. This option is deprecated since Angular 15, so provide the service in the component or the route that needs its own instance instead.

Configuring dependencies

  • Then you have to understand the different injection configurations that you can do, in fact you can configure the injection with different types of objects: 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 class without InjectionToken.
  • class: { provide: MyService, useClass: MyService } // It is also possible to use a shortcut: MyService.
  • value: { provide: 'MY_CONST', useValue: 'https://angular.dev' } // 'MY_CONST' can be declared as a string without InjectionToken.
  • value: { provide: MY_CONST, useValue: 'https://angular.dev' } // MY_CONST can be declared as InjectionToken<string>.
  • value: { provide: MY_CONFIG, useValue: { value: 'https://angular.dev' } } // MY_CONFIG must be declared as InjectionToken<MyInterface> because an interface has no runtime representation.
  • factory: { provide: MY_OBS, deps: [DOCUMENT], useFactory: doThingFactory } // MY_OBS must be declared as InjectionToken<Observable<string>> and doThingFactory is a function which returns the observable. You can also create your factory using the 2nd argument of InjectionToken. Take care to understand the difference between both: with useFactory it is not tree-shakable, you have to declare the provider manually and you can easily switch between different implementations through a different useFactory function. With the factory option of InjectionToken it is tree-shakable, the token is automatically provided in root but you can still change the implementation by declaring another provider for the token in a providers array.
  • existing: { provide: MY_TOKEN, useExisting: forwardRef(() => MyDirective) } // MY_TOKEN becomes an alias of the MyDirective instance. In general forwardRef is used when a class is referenced before its definition and it also sometimes helps to break a circular dependency easily.

Decorating dependencies

The decorators below can be used to configure the injection behavior more precisely. They can be used in the constructor method or in the deps array while providing a factory, and the same options exist for the inject() function, for instance inject(MyService, { optional: true }).

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

Learn more about DI in Angular

Learn more about dependency injection in the official Angular documentation.

Up next

Learn more about Angular