Angular Enterprise
Dependency injection in Angular
- Courses
- 20 practices
- 6 min read
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 servicesare possible since Angular version 6 by addingprovidedIn: '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 theproviders: []array ofbootstrapApplication(or of the core module) or by using theprovidedIn: 'root'syntax in their@Injectable()decorator, and then they can be used everywhere (lazy or not) without putting them in theproviders: []array of any module.Shared services(shared by multiple lazy or core modules): if you put your services inside theprovidersarray 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 isprovidedIn: 'root'. Otherwise you can use theModuleWithProvidersinterface 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 theRouterservice of theRouterModule.Feature services(lazy route or module) can be scoped to that feature by removing theprovidedIn: 'root'from their@Injectable()decorator and adding them to theproviders: []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 withprovidedIn: 'root'the Angular compiler will also do this if and only if your service is used only in this lazy feature.Component servicescan be scoped to that component by removing theprovidedIn: 'root'from their@Injectable()decorator and adding them to theproviders: []array of the component. The service will be available in all child components, the view children and the content children. In addition toprovidersyou can add aviewProvidersarray 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 theprovidersarray defined first.Platform servicesshared between multiple apps or Angular Elements. You can useprovidedIn: 'platform'in order to make a service available between multiple apps or Angular Elements.Singleton servicescan be created usingprovidedIn: 'root', this way the service will be available application wide as a singleton with no need to add it to a module'sprovidersarray as was the case on versions of Angular lower than v6.Non singleton servicescould be created usingprovidedIn: '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
InjectionTokenmechanism if the type has no runtime representation, for example an interface, otherwise you can directly pass your class withoutInjectionToken. - 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 withoutInjectionToken. - value:
{ provide: MY_CONST, useValue: 'https://angular.dev' }//MY_CONSTcan be declared asInjectionToken<string>. - value:
{ provide: MY_CONFIG, useValue: { value: 'https://angular.dev' } }//MY_CONFIGmust be declared asInjectionToken<MyInterface>because an interface has no runtime representation. - factory:
{ provide: MY_OBS, deps: [DOCUMENT], useFactory: doThingFactory }//MY_OBSmust be declared asInjectionToken<Observable<string>>anddoThingFactoryis a function which returns the observable. You can also create your factory using the 2nd argument ofInjectionToken. Take care to understand the difference between both: withuseFactoryit is not tree-shakable, you have to declare the provider manually and you can easily switch between different implementations through a differentuseFactoryfunction. With thefactoryoption ofInjectionTokenit 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 aprovidersarray. - existing:
{ provide: MY_TOKEN, useExisting: forwardRef(() => MyDirective) }//MY_TOKENbecomes an alias of theMyDirectiveinstance. In generalforwardRefis 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.