Provider type | Angular
- Provider type | Angular
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
@Injectable()
export class StorageService {
get(key: string) {
return `${key} base`;
}
}
local-storage.service.ts
@Injectable()
export class LocalStorageService extends StorageService {
get(key: string) {
return `${key} local`;
}
// there is more than what we have here
}
native-storage.service.ts
@Injectable()
export class NativeStorageService extends StorageService {
get(key: string) {
return `${key} native`;
}
// there is more than what we have here
}
Provide
provide is an option where you specify a token name. Token name can be Class, string, or InjectionToken.
Syntax
providers: [
{
provide: StorageService,
useClass: LocalStorageService,
}
],
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
@NgModule({
imports: [...],
exports: [...],
providers: [
MyService,
],
}])
export class MyModule { } -
@Injectabledecorator metadata{providedIn: 'root'}, defines tree-shakable singleton service on a root level.my.service.ts
@Injectable({providedIn: 'root'})
export class LocalStorageService implements StorageService {
get(key: string) {
return `${key} local`;
}
}
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
providers: [
{
provide: StorageService,
useClass: LocalStorageService,
// OR// useClass: WebSqlStorageService,
}
]
Usage
@Component({..})
export class MyComponent {
constructor(
private storage: StorageService,
) { }
}
String token
- Token identifier as a string
- It requires @Inject to get access to the dependency
Define String token
providers: [
{
provide: 'storage',
useClass: LocalStorageService,
}
]
Usage
@Component({..})
export class MyComponent {
constructor(
@Inject('storage') private storage: LocalStorageService,
) { }
}
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
export const StorageToken =
new InjectionToken<StorageService>('token');
providers: [
{
provide: StorageToken,
useClass: LocalStorageService,
}
]
Usage
@Component({..})
export class MyComponent {
constructor(
@Inject(StorageToken) private storage: StorageService,
) { }
}
factory function with Injection token
export const StorageToken =
new InjectionToken<StorageService>(
'token',
() => new MyService()
);
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
{
provide: StorageService,
useClass: StorageService,
}
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.
{
provide: StorageService,
useClass: FakeStorageService,
}
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
provider: [
{
provide: ApplicationConfig,
useValue: config,
},
],
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
{
provide: StorageService,
useFactory: () => {
if (isMobile) {
return NativeStorageService();
}
return LocalStorageService();
}
}
useFactory accepts a function, we did an isMobile check to conditionally instantiate an instance of desired StorageService.
Usage
@Component({..})
export class MyComponent {
constructor(
private storage: StorageService,
) { }
}
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
@NgModule({
imports: [...],
declarations: [
AppComponent,
...
]
providers: [
StorageService,
{
provide: LocalStorageService,
useExisting: StorageService,
},
...
],
exports: [...],
})
export class AppModule { }
app.component.ts
@Component({...})
export class AppComponent {
constructor(
private storage: LocalStorageService,
) {}
onSubmit() {
console.log(this.storage.get('MY_KEY'));
}
}
output
MY_KEY base
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.
@NgModule({
providers: [
StorageService,
{
provide: StorageService,
useClass: LocalStorageService,
},
{
provide: StorageService,
useClass: NativeStorageService,
},
...
],
})
export class AppModule { }
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
💡 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}
@Injectable({providedIn: DashboardModule})
export class DashboardService { }
NgModule's provider option
@NgModule({
imports: [],
declarations: [],
providers: [
DashboardService,
],
})
export class DashboardModule { }
{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.
@Injectable({providedIn: 'root'})
export class DashboardService { }
{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.
@Injectable({providedIn: 'any'})
export class DashboardService { }
{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.
@Injectable({providedIn: 'platform'})
export class DashboardService { }
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
@Component({ ... })
export class MyComponent {
constructor(
private storage: StorageService,
) { }
onSubmit() {
console.log(this.storage.get('access_token'));
}
}
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.
@Component({ ... })
export class MyComponent {
constructor(
private inejctor: Injector,
) { }
onSubmit() {
const storage = this.injector.get(StorageService);
console.log(storage.get('access_token'));
}
}
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
@Component({ ... })
export class MyComponent {
storage = inject(StorageService);
onSubmit() {
// below is not allowed 🚫.// const storage = inject(StorageService);
console.log(this.storage.get('access_token'));
}
}
Service as Singleton
Service is singleton in nature. Take an example of the Below module.
@NgModule({
declaration: [
ChartComponent,
DashboardComponent,
],
providers: [
DashboardService,
],
...
})
export class DashboardModule { }
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.