Introduction
In the angular framework, dependency injection, and providers are one of the core features. This feature allows us to use a single data source in many places. For example, we can share one HTTP call result with many components to avoid too many requests for the same data. Another example could be to override one value or a class with another. In this article, I will explain the providers, how to define them, and how to use them in many places.
What are the providers?
Providers in Angular are a source of value that could be shared between many recipients. It could be a static value like, for example, string
or boolean
, or it could be a whole class that uses the same provider instance. In the Angular role of provider, classes are covered by services. Services are a special kind of class with an Injectable
decorator. Thanks to that, we can use other providers inside our class because they can be injected. By default, the Injectable
decorator takes as an argument for the providedIn
value root
, meaning that this current service will be initiated at the root level of the app. More details about that You can find later in this article.
Type of providers
The angular framework has a few types of providers which work differently but, in the end, serve data.
Injection token
One of the possible injectors is the InjectionToken
.
It is the simplest provider whose main purpose is to represent values not contained in any class.
An example definition of the injection token is:
export const timezoneToken = new InjectionToken<string>('My timezone');
It is generic and accepts type definition and My timezone
as a description. It is useful when we forget about implementing a given injection token and the error contains a given description.
Value provider
The value provider is a provider who accepts the useValue
property value that will be set for the provider set in the provide
property. It can be any of the values. An example definition can be:
{
provide: timezoneToken,
useValue: 'europe/warsaw'
},
I used the token from the previous example as a provider and set europe/warsaw
as a value.
Factory provider
Sometimes we want to do something with an existing provider and provide it as another new provider instance. We can use a factory provider, which accepts three properties for that purpose. The provide
property defines which provider we will provide. Let's make an example. First of all, I will define another injection token:
export const timezoneCityToken = new InjectionToken<string>('My timezone city');
Then to define the factory provider:
{
provide: timezoneCityToken,
useFactory: (timezone: string) => {
return timezone.split('/')[1];
},
deps: [timezoneToken]
},
In the deps
property, we set dependencies for our provider. In this example, I used timezoneToken
from the previous example. Those dependencies are passed as params to the method, which is set in the useFactory
property. That property accepts a method that returns the value for our new provider.
Sometimes we want to set an array of values under the same provider. To achieve that, We can set property multi
to true
, so the value of the above provider would be ['warsaw']
instead of just warsaw
.
Use class provider
Another type of provider is the useClass
provider. It is used when we want to use a standard class to create an instance for our provider. An example of usage can be:
{
provide: MyDataAbstarct,
useClass: UsersList,
}
And the MyDataAbstarct
content is an abstract class, so it cannot have its own instance:
export abstract class MyDataAbstarct {
abstract itemsList: string[];
}
And the UsersList
content:
export class UsersList extends MyDataAbstarct {
itemsList = ['John', 'Mike', 'Anna'];
}
To reach this list, we can utilize the inject
method by:
readonly list = inject(MyDataAbstarct).itemsList;
So useClass
provider is a cool way to have consistency across different places and use common patterns because we can easily replace the used class but keep the provider.
Use existing provider
The last type of provider is the useExisting
provider. It works similarly to a useClass
, but it shares the same instance with the selected provider. Simple usage can be:
...,
UsersList,
{
provide: MyDataAbstarct,
useExisting: UsersList,
},
...
Please remember that useClass
would create a separate instance of the selected provider.
Providers usage
Those are examples of which types of providers exists in Angular. Now let's see how we can provide them.
Root level
Services in Angular are classes that can be injected with other providers and are mainly used to serve common logic. We can inject them (to create new instances) at the root level of our application, which is a default approach for services. It is set by a dedicated decorator:
@Injectable({
providedIn: 'root'
})
Value root
means that a given service will be injected at the app's top level. Another value can be platform
, meaning we share instances with other apps on the same page. The last possible value is any
, meaning that each place will receive a new, unique service instance.
Module level
We can provide any provider at the module level by defining it in providers
property, for example:
@NgModule({
declarations: [
FlagComponent
],
imports: [
],
providers: [FlagService],
})
export class FlagModule { }
That means FlagService
will provide the same instance across all modules and components used in FlagModule
.
Component level
Like above, we can use the provider on a component level by adding it to the providers
property. The same instance and all children's components will be shared inside that component.
Route level
To decrease imports and lines of code in components, We can set providers on the route configuration, for example:
export const appRoutes: Routes = [
{
path: 'flag',
loadChildren: () => import('./flag/flag.module').then(m => m.FlagModule),
providers: [
FlagService,
],
}
]
It works in a similar way as provided on the component level.
To access a provider, we can utilize two ways:
-
inject
function in the class body oruseFactory
provider, for example:
readonly myInstance = inject(MyService);
-
constructor
, for example:
constructor(readonly myInstance: MyInstance) {}
Providers direct usage configuration
Sometimes we want to omit a provider's usage, or it can be optional. Angular has dedicated flags for providers to set it up.
-
Optional
- marks provider as optional and won't throw an error when not provided but returnsnull
instead, -
Self
- will look for a provider only at the current component or directive level -
SkipSelf
- will skip the current component or directive and look for the provider at the parent level, -
Host
- will look for a provider up to the host level.
To apply those flags with the inject
method, we can add them as a configuration object, for example:
readonly list = inject(MyDataAbstarct, { optional: true })?.itemsList;
Or if we are using a constructor, then we have to add a dedicated decorator:
constructor(
@Optional() readonly list: MyDataAbstarct,
) {}
Summary
Angular has a powerful mechanism of providing values and shares common sources with many ways of configuring it. Knowing it allows developers to write reusable and clean code. Here You can find more details.
Top comments (0)