Working with providers in Angular

Sergey Gultyayev
9 min readMar 30, 2023

--

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) {}
}
  1. The newest approach is to use an inject function and pass a reference to a class we want to inject an instance of.
  2. 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 a constructor 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 use inject 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 without loadChildren ) they all are merged into one single module. That’s why when you write imports: [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 a ServiceA you want to add to the ComponentA ’s metadata providers: [ServiceA] . This way no matter how many ComponentA ’s there are on the page — each one of them will receive a separate instance of the ServiceA. When ComponentA gets destroyed (removed from DOM) its ServiceA is being destroyed as well.
  • When you have a so-called Module with providers (e.g. HttpModule) you can import it in the AppModule 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 and providers 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 write providedIn: '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 write providers: [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 return null 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 provided InjectionToken . 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 of OldLogger will automatically get a NewLogger ‘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:

  1. Define an injection token (it can be a class too).
  2. Provide a value ( { provide: myToken, useClass/useValue/useFactory/useExisting: ... } ).
  3. 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).

--

--

Sergey Gultyayev
Sergey Gultyayev

Written by Sergey Gultyayev

A front-end developer who uses Angular as a main framework and loves it

Responses (2)