Working with providers in Angular
When you just come to Angular it may be overwhelming to understand these providers: [Service1, Service2]
that can be used in Modules, Directives, and Components metadata. No wonder it may appear intimidating when you encounter a whole object instead of a class. Let’s sort it out.
What a provider is
First off, let’s recap what a provider is. Simply put, it’s an instance of a class that can be accessed across the app. We can do so by one of the two approaches:
@Component({...})
export class AppComponent {
private service1 = inject(Service1)
constructor(private service2: Service2) {}
}
- The newest approach is to use an
inject
function and pass a reference to a class we want to inject an instance of. - The older one which you will encounter most of the time is to use
constructor
and use a class reference as a type of argument.
Fun fact.
When aconstructor
is used it’s using internally a library called “reflect-metadata” which extracts a class reference into a component’s metadata so that it’s persistent in the JS build (JS has no types). This is why you cannot use interfaces as injection tokens in your constructors as those don’t exist in the final build.
When you useinject
function it’s a pure JS function which doesn’t need the lib (you still cannot use an interface as its argument :) ). It in itself doesn’t make one better than the other, though.
How to become a provider
Not every class can be injected. Only those that are present in the dependency injection tree. Components and Directives are mounted and become accessible for injection automatically. You cannot inject undecorated class, however. Therefore, you need to mark it as @Injectable()
for Angular’s compiler to know that it can injected.
There is more to it. You cannot add the @Injectable()
decorator and inject the class immediately. It will throw an error. This happens, because Angular needs to know where to put it in the injection tree. The most common way to “fix” it is to specify @Injectable({ providedIn: 'root' })
. When done so — Angular knows that when this class’ instance can be put in the root module. For most of the use cases it’s a well-round approach.
Provider scopes
When you use providedIn: "root"
Angular will create only one instance of the class. This pattern when the whole app is using only one instance of a given class is called Singleton.
However, you may need to have a new instance of a service for a module/component. The most common case is when the service contains state scoped to that component/module (state that is separate from the rest of the app).
For example, if you allow to open and edit multiple User
s via tabs. Then you need to make sure that each UserComponent
gets their own UserService
. To do so you specify the service in the component’s providers: []
array. This way when the UserComponent
or its children inject the service Angular knows that it should go no further than UserComponent
to get the instance of the service. If there is no instance — it will create one, attach it to the UserComponent
’s injector and return the instance where it was requested.
Beware! An instance of a service is only created when it was injected. This means, that if you have some logic in a service’s constructor and never inject this service — it’s instance will never be created! Therefore, if you want your service for analytics or similar — you will have to inject it in the AppComponent for instance to be created and its initialisation logic to be run.
Modules, Components and Directives all have the possibility to declare providers: []
array in their metadata. It works the same as for the example above. All subtree will receive an instance that is scoped to the Module/Component/Directive.
Important note. When using Module to specify its providers you should know that eagerly loaded modules (when they are in the
imports: []
array or specified in the router withoutloadChildren
) they all are merged into one single module. That’s why when you writeimports: [HttpModule]
in the AppModule its provider (HttpClient) is available in your whole application.
If you specify your provider in a lazily loaded module — the provider is scoped to that module and its tree.
Now, let’s take a deep breath. The previous section is rather overwhelming if you aren’t familiar with Angular’s DI (Dependency Injection) close and that’s OK. So don’t be discouraged as you will eventually grasp it as you continue working with Angular and experimenting with its DI.
For now you need to know that:
- When you need a single instance of a service for your app just use
Injectable({ providedIn: 'root' })
and it will work just fine. - When you want each
ComponentA
to have its own instance of aServiceA
you want to add to theComponentA
’s metadataproviders: [ServiceA]
. This way no matter how manyComponentA
’s there are on the page — each one of them will receive a separate instance of theServiceA
. WhenComponentA
gets destroyed (removed from DOM) itsServiceA
is being destroyed as well. - When you have a so-called Module with providers (e.g.
HttpModule
) you can import it in theAppModule
and its providers will be available across the whole app. If you import the module in a lazily-loaded module — its providers will be scoped to that module and its children.
Providers array vs providedIn
It’s preferred to use providedIn: 'root'
for services because when you don’t use a service in the app — it’s tree-shaken (removed from the final build) thus you don’t bring dead code to the production build.
Only when you need an instance of a service to be created in a scope of component/module/directive and not global scope you should use providers: []
array. But keep in mind that services specified in providers
array cannot be tree-shaken.
The difference in tree-shakeability of
providedIn
andproviders
lies in how bundlers work. They decide whether to include a piece of code into the bundle based on whether this code was referenced from the code. When you writeprovidedIn: 'root'
and don’t use the service anywhere — there is no code referencing the service and thus it’s not included in the bundle. However, when you writeproviders: [MyService]
you reference your service from the code and hence bundler suggests that you use this service and so it has to be included in the bundle regardless of whether you inject it anywhere across the app.
Injecting children
Angular’s DI system is great. Not only it allows you to inject services (and also parent components/directives), but also inject components/directives/services that are in the template of the component or are enclosed in the component’s tag (e.g. in case of <my-component><my-child /></my-component>
MyComponent can inject MyChild via ContentChild
).
How does Angular know where to look for a dependency? That’s easy. When you use inject()
or constructor
then it will always be seeking for the dependency towards the root. If you use ViewChild
/ ViewChildren
it will look for the dependency in the component’s template. When you specify ContentChild
/ ContentChildren
it will look for the dependency in the tree enclosed by component’s tags.
When you work with ViewChild(ren)/ContentChild(ren) you don’t look at it as DI (at least I didn’t). You use them to get access to the DOM elements, to template references, sometimes to components or directives. However, the fact that you can also inject Services that are provided (providers: []
) in the children gives a hint that it really is a DI and not some mere querySelector
.
When I say that you can it doesn’t mean that you should inject your children. Doing so would make it more difficult to follow the business logic.
Injection flags
When you use a constructor
or inject
to get an instance of a class you can also specify so called injection flags. Doing so enables you to direct DI the way you want it. Let’s have a look at flags that we can use:
Host
— DI will go no further than the host of the current component/directive.Self
— DI will go no further than the current Component/Directive.SkipSelf
— DI will skip the node that requested a dependency.Optional
— if no dependency matched it will returnnull
for its value instead of throwing an Error.
To use flags you simply specify them as decorators in the constructor
or as a second argument to inject
function in form of an object where key is a lowercased name of the flag and the value is boolean determining the application of the flag.
You can use multiple flags at the same time. For example:
@Component({...})
export class AppComponent {
constructor(@Optional() @SkipSelf() private service: Service) {}
}
// same as
@Component({...})
export class AppComponent {
private service = inject(Service, { optional: true, skipSelf: true })
}
In the example above we tell Angular that we want to skip current component from service lookup and also that it should give us null
instead of throwing an Error in case it cannot find the Service
.
Self
, SkipSelf
, Host
aren’t used in the day-to-day work for most of the projects, because you have a simple dependency tree. You need them when you have a tree which has multiple instances of the same service. E.g. you could have a component tree where ComponentA
can render recursively and it has a ServiceA
in providers
array. In order to access parent’s service and not its own service you will use SkipSelf()
flag and optionally Host()
.
Optional
is useful when you have a library that can be configured by providing a configuration. To make this configuration optional and use some defaults instead you will have to specify Optional()
flag so that app doesn’t crash when there is no config provided.
Injection tokens
One of the key building blocks of DI is an injection token. It may sound scary, however in reality it’s pretty simple. Injection token — is a value that stays in JS after the build. It means that it can be a plain string, a class, an object (instance of InjectionToken
). When you write providers: [ServiceA]
or inject(ServiceA)
you still use an injection token and its resolved to class ServiceA
.
However, sometimes you want to inject a value that is not a class’ instance. This is where you want to use a string or InjectionToken
instance.
Usage of plain string tokens is discouraged. Instead, you should use an instance of InjectionToken.
A good example of how it can be used is provided in the docs of the InjectionToken
.
class MyService {
constructor(readonly myDep: MyDep) {}
}
const MY_SERVICE_TOKEN = new InjectionToken<MyService>('Manually constructed MyService', {
providedIn: 'root',
factory: () => new MyService(inject(MyDep)),
});
This will create a tree-shakeable injection token (same as if you use @Injectable({ providedIn: 'root' })
), but can be used with non-classes.
To use an InjectionToken
you will need to provide its value so that Angular know how to resolve the dependency. To do so you need a providers
array and a provider object. It has a following interface
{
provide: injectionToken,
useValue: '', // or
useFactory: () => '', // or
useExisting: MyService
}
useValue
— is the most basic one. It will return to the DI any value you assign to it.useClass
— it will create an instance of a class that you provide.useFactory
— it’s a function that has to return some value (it’s synchronous). This is a great place to provide a platform-dependent value. This is executed only when an instance is being created. Not each time it’s accessed. Therefore, you cannot use it to change the value in run time.useExisting
— you pass an already providedInjectionToken
. This can be used when you use an old library and want to upgrade it. Instead of going to refactor everything, you could provide a new service with an interface which is identical or extends the old one. In the example below any usages ofOldLogger
will automatically get aNewLogger
‘s instance with no refactoring required (it’s better to do it eventually :) ).
@Module({
providers: [
NewLogger,
{
provide: OldLogger,
useExisting: NewLogger
}
]
})
export class AppModule {}
multi: true
If you have written an HttpInterceptor
you could have encountered usage of multi: true
in the provider’s configuration. When you use true
for the value it cannot be undone. It’s either all of the providers with such injection token true
or false
. When the value is true
— Angular’s DI will return all dependencies with such injection token up the tree. Thus, where you inject()
such dependency you will receive an array even if there is only one provider. That’s why you can add multiple HttpInterceptor
to the app.
It’s a great way to extend the functionality of an underlying library without the need to interfere with its internal code. It’s a perfect example of open-closed principle in the wild.
You can also consume such dependencies. To do so you need:
- Define an injection token (it can be a class too).
- Provide a value (
{ provide: myToken, useClass/useValue/useFactory/useExisting: ... }
). - Specify
multi: true
on the provider.
Now, you are ready to consume your token.
@Injectable({...})
export class MyConsumerService {
constructor(@Inject(myToken) private services: Service[]) {}
}
// or
@Injectable({...})
export class MyConsumerService {
private services: Service[] = inject<Service[]>(myToken);
}
When working with multi providers we have to manually typecast the value to an array and also manually specify the injection token (in case of constructor).