Providers in Angular
In Angular, 'providers' are key to its Dependency Injection (DI) system, creating and configuring dependencies for applications. Providers, essentially a collection of Classes/Tokens, facilitate object creation and ensure singleton instances. They're injected into constructors, akin to DI container bindings, defining how dependencies are resolved. This article delves into Angular's powerful DI and providers practical applications.
What are Angular Providers?
The Angular Provider is a mapping of tokens that helps Dependency injection in how object creation should happen. The mapping is a collection of an array of providers. Each provider is uniquely identified as a token.
When we inject the provider on the class level constructor, it looks up for providers collection to instantiate dependency token/class. And then, that instance can be utilized inside a Component, Service, Directive, Pipe, etc.
Whenever any token is injected as a parameter in the constructor, for resolving that token, DI refers to the current Module Injector Tree branch. If not found in the current injector tree, it goes to the parent to resolve the dependency. If the dependency is not resolved, it throws a StaticInjector error.
Configuring the Angular Provider
There are different ways to configure a provider. A provider can configure it conditionally. It can be of various types like string, boolean, date, custom type, etc. Even one can use direct const values to assign to the provider.
For eg, We will use the below example for future examples. We have StorageService and there are two implementations of StorageService. Like LocalStorageService and WebSqlStorageService.
Dependencies can be configured on the below levels.
- NgModule level providers array.
- Component level providers array.
storage.service.ts
local-storage.service.ts
native-storage.service.ts
Provide
provide is an option where you specify a token name. Token name can be Class, string, or InjectionToken.
Syntax
StorageService is a token and we are telling DI to create an instance of the LocalStorageService class dependency whenever StorageService is injected.
Provider
- Where we provide an implementation of token / Class
-
Define Provider in @NgModule or @Component decorator metadata.
my.module.ts
-
@Injectable decorator metadata {providedIn: 'root'}, defines tree-shakable singleton service on a root level.
my.service.ts
DI Token
- Token-based DI instances creation
- Ask for instance based on token (component, directive, template, selector, etc)
- Singleton
Type Token
- Type token can be anything
- Component, Directive, Template, selector.
Syntax
Usage
String token
- Token identifier as a string
- It requires @Inject to get access to the dependency
Define String token
Usage
Injection Token
- Now, you can define the string token with its type/custom_type
- We can also call it an improved version of the string token.
- It provides an object to pass factory for defining token.
- It also requires @Inject to use it.
Define Injection token
Usage
factory function with Injection token
The Types of Provider
There are different ways to describe providers
Class Provider: useClass
- In this case, we provide an implementation of the Provider token by passing Class inside a useClass option.
useClass Example
Switching Dependencies
- By principle, Dependency Injection is focused on loosely coupling.
- You can easily mock/fake the token provider, with mock implementation for testing purposes.
Value Provider: useValue
- Sometimes we don't need a class, but just an object.
- useValue can be utilized for the same use case.
- pass a plain object to the useValue function, and that will be considered as the evaluated value of dependency.
- useValue don't accept a function.
UseValue Example
Suppose we need to load some application configuration object, we can simply use useValue to determine the value of token while initializing the angular application.
Factory Provider: useFactory
There are certain use cases where we had to rely on different implementations. For eg. StorageService is a service that helps to persist data for an application. But when it runs on a different platform, we need a separate implementation for the same.
for Browser => requires LocalStorage for NativeApp => requires NativeStorage
This is the perfect use case for useFactory usage. We can use useFactory to conditionally decide which dependency to be instantiated.
UseFactory example
useFactory accepts a function, we did an isMobile check to conditionally instantiate an instance of desired StorageService.
Usage
useFactory Vs useValue
useFactory | useValue |
---|---|
Helps DI to decide dependency dynamically | Directly pass POJO as dependency |
always accept a function | always accept a POJO object |
Can have access to other dependencies using deps option | Don't have access to other dependencies during generation |
Aliased Provider: useExisting
- It can be used to alias an existing dependency instance.
- It does not create an instance.
UseExisting Example
app.module.ts
app.component.ts
output
When a dependency is injected inside an AppComponent constructor, it refers to AppModule where AppComponent has been declared. For evaluation of dependency, it lookups providers array. There is a LocalStorageService that provide a token, with useExisting of StorageService. So what happens is, that it does not create an instance of LocalStorageService, instead, it refers to the StorageService instance. In other words, we can call it LocalStorageService as an alias of StorageService.
Multiple Providers with the Same Token
If you register multiple providers in angular NgModule with the same token, the compiler won't make any complaints about it. It is okay to have multiple registrations for a single token. The latest token, i.e. registered, should win and create the instance of the same.
In the above example when StorageService is injected, it will refer to the last provider registration. Where we would get an instance of NativeStorageService.
Registering the Dependency at Multiple Providers
Provider scope varies based on where we define the dependency.
Provider Scope
- Root NgModule - Root module providers can be used to module instantiated inside it. The same applies to all eagerly loaded NgModules
- Lazily Loaded NgModule - This module creates a separate injector tree, so there providers inside lazily loaded modules refer to the current context first.
- Define providers on the Component, Directive or Pipe level
:bulb: Dependency lifetime, is decided based on where the dependency is defined.
Module Level
- It can be defined using @Injectable metadata {providedIn: DashboardModule}
- It instantiates service instances per module in the lazily loaded module
- Another way to define a provider on the module level is, to mention dependency in DashboardNgModule's providers array.
@Injectable {providedIn: module}
NgModule's provider option
{providedIn: 'root'}
- DI generates a single instance of service throughout the bootstrapped application context.
- Even lazily loaded modules will get access to the same instance.
{providedIn: 'any'}
- This dependency object will create an instance for a module it is being resolved.
- We can call it a dependency for including lazily loaded modules.
{providedIn: 'platform'}
- A special singleton platform injector shared by all angular applications loaded on the page.
- This can be relatively a lot helpful when multiple angular elements wanted to communicate with each other.
Service Injection Techniques
There are various ways to inject a dependency inside a class.
constructor()
- Declare a parameter on the constructor level with its type
injector.get()
- Declare a parameter on the constructor level called injector.
- injector is aware of all the providers belonging to it
- Call the injector.get method and pass the token to retrieve the dependency instance.
inject()
- Property level injection
- wrap the token inside the inject function, and get an instance of the dependency
- inject function can not be used inside any other function than the constructor
Service as Singleton
Service is singleton in nature. Take an example of the Below module.
Now suppose, ChartComponent inject DashboardService in the constructor parameter, it creates an instance of DashboardService. But next time when ChartComponent injects DashboardService. It goes to DI, DI checks whether the DashboardService instance is already created for this service or not. If yes, then it returns the earlier created instance. This way we had only generated the DashboardService instance once. That's why we call services singleton in Angular.
Conclusion
- Define the provider as a Singleton Service
- provider scope can be controlled where and how it is defined.
- Different ways to configure the Providers in angular
- Configuring multi-provider with dependency.