One of the most powerful and distinguishing features of Angular is its built-in dependency injection system.
Most of the times, dependency injection just works, and we use it almost without thinking thanks to its very convenient and intuitive Angular API.
But there are other times, where we might need to dig a lot deeper into the dependency injection system, and configure it manually.
This in-depth knowledge of dependency injection will be necessary for example in these circumstances:
- to troubleshoot some weird dependency injection error
- to configure manually the dependencies for a unit test
- to understand the unusual dependency injection configuration of some third-party module
- to create a third-party module that is going to be shipped and used in multiple applications
- to design your application in a more modular way
- to ensure that different parts of your application are well isolated and can't interfere with each other
In this guide, we are going to understand exactly how Angular dependency injection works, as we are going to cover all its configuration options and learn when to use each feature and why.
We are going to do this in a very practical and easy-to-understand way, by implementing our own providers and injection tokens from scratch, as an exercise, and covering all features with an example-based approach.
This deep understanding of the Angular dependency injection system is going to prove invaluable to you as an Angular developer over time. 😉
Table Of Contents
In this post, we will cover the following topics:
- Introduction to Dependency Injection
- How to set up dependency injection from scratch in Angular?
- What is an Angular Dependency Injection Provider?
- How to write our own provider?
- Introduction to Injection Tokens
- How to manually configure a provider?
- Using class names as Injection Tokens
- Simplified configuration of providers: useClass
- Understanding Angular multiple value dependencies
- When to use an
useExisting
provider - Understanding Angular Hierarchical Dependency Injection
- What are the advantages of Hierarchical Dependency Injection?
- Components Hierarchical Dependency Injection - an example
- Modules Hierarchical Dependency Injection - an example
- Modules vs Components Dependency Injection Hierarchies
- Configuring the Dependency Injection Resolution mechanism
- Understanding the @Optional decorator
- Understanding the @SkipSelf decorator
- Understanding the @Self decorator
- Understanding the @Host decorator
- What are Tree-Shakeable Providers?
- Understanding Tree-Shakeable Providers via an example
- Summary
This post is part of our ongoing series on Angular core features, you can find all the articles available here.
So without further ado, let's get started learning everything that we need to know about Angular dependency injection! 😃
Introduction to Dependency Injection
So what exactly is dependency injection?
When you are developing a smaller part of your system, like a module or a class, you are going to need some external dependencies.
For example, you might need an HTTP service to make backend calls, as well as some other dependencies.
You might even be tempted to create your own dependencies locally every time that you need them, like this:
This might look like a very simple solution, but there is a problem with this code: it's super hard to test.
Because the code knows about its own dependencies and creates them directly, it's impossible with this version of the code to replace for example the actual HTTP client with a mock HTTP client, and unit test the class.
Notice that this class knows not only how to create its own dependencies, but it also knows about the dependencies of its dependencies 🙄, meaning it knows about the dependencies of the HTTPClient class as well.
With the way that this code is written, it's basically impossible to replace at runtime this dependency with an alternative version, for example:
- for testing purposes
- but also because you might need different HTTP clients in different runtime environments, like for example on the server vs the browser.
Compare this to an alternative version of this same class, that does use dependency injection:
As you can see in this new version, this class simply does not know how to create its http dependency.
This new version of the class simply receives all the dependencies that it needs as input arguments in the constructor, and that's it! 😃
This new version of the class just knows how to use its dependencies to perform a concrete task, but it does not know how that dependency works internally, how it was created, and what are its dependencies.
The code to create the dependency has been moved out of the class and placed somewhere else in your code base, thanks to the use of the @Injectable()
decorator.
With this new class, it's super easy to:
- replace an implementation of a dependency for testing purposes
- to support multiple runtime environments
- to provide new versions of a service to a third-party that uses your service in their code base, .etc.
This technique of just receiving your dependencies as inputs without knowing how they work internally or how they were created is aptly named dependency injection, and it's one of the cornerstones of Angular.
Let's now learn exactly how Angular dependency injection works.
How to set up dependency injection from scratch in Angular?
The best way to understand dependency injection in Angular, is to take a plain Typescript class without any decorators applied to it, and turn it into an Angular injectable service manually, from scratch.
It's much easier than it sounds. 😃
Let's start with a very simple service class, with no @Injectable()
decorator applied to it:
As we can see, this is just a plain Typescript class, that expects some dependencies to be injected in its constructor.
But this class is in no way linked to the Angular dependency injection system at all.
Let's see what happens if we now try to inject this class as a dependency into the constructor of another class:
As we can see, we are trying to inject an instance of this class as a dependency.
But our class is not linked to the Angular dependency injection system, so what part of our program will know how to call the CoursesService
constructor to create an instance of that class and pass it as a dependency?
The answer is simple: nobody, and so we will get an error! 😉
NullInjectorError: No provider for CoursesService!
Notice the error message: apparently something known as a provider is missing.
You probably have seen a similar message before, it happens very commonly during development.
Let's now understand exactly what this message means, and how to troubleshoot it.
What is an Angular Dependency Injection Provider?
The error message "no provider" means simply that the Angular dependency injection system can't instantiate a given dependency, because it does not know how to create it.
In order for Angular to know how to create a dependency such as for example the CoursesService
instance injected in the CourseCardComponent
constructor, it needs what is known as a provider factory function.
A provider factory function is simply a plain function that Angular can call in order to create a dependency, it's as simple as that: it's just a function. 👍
That provider factory function can even be created implicitly by Angular using some simple conventions that we will talk about, and that is actually what usually happens for most of our dependencies.
But we can also write that function ourselves if needed.
In any case, it's important to understand that for every single dependency in your application, be it a service or a component or anything else, there is somewhere a plain function that is being called that knows how to create your dependency.
How to write our own provider?
To really understand what a provider is, let's simply write our own provider factory function for the CoursesService
class:
As you can see, this is just a plain function that takes as input any dependencies that CoursesService
needs.
This provider factory function will then call the CoursesService
constructor manually, pass all the needed dependencies, and return the new CoursesService
instance as the output.
So any time that the Angular dependency injection system needs an instance of CoursesService
, all it needs to do is to call this function!
This looks very simple, but the problem is the Angular dependency injection system does not know about this function yet.
More important than that, even if Angular knew about this function, how would it know that it needs to call it to inject this particular dependency:
I mean, there is no way for Angular to make the link between this injected instance of CoursesService
and the provider factory function, right?
Introduction to Injection Tokens
So how does Angular know what to inject where, and what provider factory functions to call to create which dependency?
Angular needs to be able to classify dependencies somehow, in order to identify that a given set of dependencies are all of the same type.
In order to uniquely identify a category of dependencies, we can define what is known as an Angular injection token.
Here is how we create our injection token manually, for our CoursesService
dependency:
This injection token object will be used to clearly identify our CoursesService
dependency in the dependency injection system.
The dependency injection token is an object, so in that sense it's unique, unlike a string for example.
So this token object can be used to uniquely identify a set of dependencies.
So how do we use it?
How to manually configure a provider?
Now that we have both the provider factory function and the injection token, we can configure a provider in the Angular dependency injection system, that will know how to create instances of CoursesService
if needed.
The provider itself is simply a configuration object, that we pass on to the
providers
array of a module or component:
As we can see, this manually configured provider needs the following things defined:
-
useFactory
: this should contain a reference to the provider factory function, that Angular will call when needed to create dependencies and inject them -
provide
: this contains the injection token linked to this type of dependency. The injection token will help Angular determine when a given provider factory function should be called or not -
deps
: this array contains any dependencies that theuseFactory
function needs in order to run, in this case the HTTP client
So now Angular knows how to create instances of CoursesService
, right?
Let's see what happens if we now try to inject an instance of CoursesService
in our application:
We might be a bit surprised to see that the same error message still occurs:
NullInjectorError: No provider for CoursesService!
So what is going on here? Didn't we just define the provider?
Well, yes, but there is no way for Angular to know that it needs to use our particular provider factory function when attempting to create this dependency, right?
So how do we make that link?
We need to explicitly tell Angular that it should use our provider to create this dependency.
We can do so by using the @Inject
annotation, everywhere where CoursesService
is being injected:
As we can see, the explicit use of the @Inject
decorator allows us to tell Angular that in order to create this dependency, it needs to use the specific provider linked to the COURSES_SERVICE_TOKEN
injection token.
The injection token uniquely identifies a dependency type from the point of view of Angular, and that is how the dependency injection system knows what provider to use.
So now Angular knows what provider factory function to call to create the right dependency, and it goes ahead and does just that.
And with this, our application is now working correctly, no more errors! 😉
I think now you have a good understanding of how the Angular dependency injection system works, but I guess you are probably thinking:
But why don't I ever have to configure providers manually?
You see, even though you usually don't have to configure provider factory functions or injection tokens manually yourself, this is what is actually happening under the hood.
For every single type of dependency of your application, be it a service, a component or something else, there is always a provider, and there is always an injection token, or some other mechanism of uniquely identifying a dependency type.
This makes sense, because the constructors of your classes need to be called by some part of your system, and Angular always needs to know what dependency type to create, right?
So even when you configure your dependencies in a simplified way, there is always a provider under the hood.
To better understand this, let's progressively simplify the definition of our provider, until we reach something that you are much more used to.
Using class names as Injection Tokens
One of the most interesting features of the Angular dependency injection system, is that you can use anything that is guaranteed to be unique in the Javascript runtime in order to identify a type of dependency, it doesn't have to be an explicit injection token object.
For example, in the Javascript runtime, class names are represented by constructor functions, and a reference to a function like for example its name is guaranteed to be unique at runtime.
A class name can then be uniquely represented at runtime by its constructor function, and because it's guaranteed to be unique, it can be used as an injection token.
So we can simplify a bit the definition of our provider, by taking advantage of this powerful feature:
As we can see, we are no longer using the COURSES_SERVICE_TOKEN
injection token object that we have manually created as a means to identify our dependency type.
In fact, we have removed that object from our code base altogether, because for the particular case of service classes, we can use the class name itself to identify the dependency type!
But if we try our program without any further modifications, we would get again the "no provider" error.
In order to make things work again, you need to use the CoursesService
constructor function to also identify which dependencies you need:
And with this, Angular knows what dependency to inject, and everything is working again as expected! 😉
So the good news is that we don't need, in most cases, to explicitly create an injection token.
Let's now see how can we further simplify our provider.
Simplified configuration of providers: useClass
Instead of explicitly defining a provider factory function with useFactory
, we have other ways to tell Angular how to instantiate a dependency.
In the case of our provider, we can use the useClass
property.
This way Angular will know that the value that we are passing is a valid constructor function, that Angular can simply call using the new
operator:
And this already greatly simplifies our provider, as we don't need to write a provider factory function manually ourselves! 👍
Another super convenient feature of useClass
is that for this type of dependencies, Angular will try to infer the injection token at runtime based on the value of the Typescript type annotations.
This means that with useClass
dependencies, we don't even need the
Inject
decorator anymore, which explains why you rarely see it:
So how does Angular know which dependency to inject?
Angular can determine this by inspecting the type of the injected property which is CoursesService
, and using that type to determine a provider for that dependency.
As we can see, class dependencies are much more convenient to use, as opposed to having to use @Inject
explicitly! 👍
For the particular case of useClass
providers, we can simplify this even further.
Instead of defining a provider object manually, we can simply pass the name of the class itself as a valid provider configuration item:
Angular will determine that this provider is a constructor function, so Angular will inspect the function, it will then create a factory function determine the necessary dependencies, and create an instance of the class on demand.
And this happens implicitly just based on the name of the function.
This is the notation that you usually use in most cases, which is super simplified and easy to use! 😉
With this simplified notation, you won't even be aware that there are providers and injection tokens behind the scenes.
But notice that just setting your provider like this won't work, because Angular will not know how to find the dependencies of this class (remember the deps
property).
For this to work, you need to also apply the Injectable()
decorator to the service class:
This decorator will tell Angular to try to find the dependencies for this class by inspecting the types of the constructor function arguments at runtime!
So as you can see, this very simplified notation is how we usually use the Angular dependency injection system, without even thinking about all the nuts and bolts that are being used under the hood. 😉
One thing to bear in mind is that the useClass
option will not work with interface names, it works only with class names.
This is because an interface is a compile-time only construct of the Typescript language, so the interface does not exist at runtime.
This means that an interface name, unlike a class name (via its runtime constructor function) can't be used to uniquely identify a dependency type.
Besides its base concepts of provider, dependencies and injection tokens, there are a few other things that are important to bear in mind about the Angular dependency injection system.
Understanding Angular multiple value dependencies
Most of the dependencies in our system will correspond to only one value, such as for example a class.
But there are some occasions where we want to have dependencies with multiple different values.
A very common example that you might have already come accross are form control value accessors.
These are special form directives that bind to a given form control and make the form control value available to the Forms module.
The problem is, there is not just one directive like this, there are many of them.
But it wouldn't be very practical to configure all these dependencies separately, because you usually want to access them all together at once.
So the solution is to have a special type of dependency that accepts multiple values, not just one, linked to the exact same dependency injection token.
In the particular case of form control value accessors, that special token is the NG_VALUE_ACCESSOR
injection token.
For example, here is an example of a custom form control component that wants to registers itself as a control value accessor:
Notice that we are defining here a provider for the NG_VALUE_ACCESSOR
injection token.
But if we would not use the multi
property, the existing value for this injection token would have been overwritten (more on this later).
But because multi
is set to true, we are actually adding to the array of values of this dependency, instead of overwriting it.
Any component or directive that needs all the control value accessors, will receive them by requesting the injection of the NG_VALUE_ACCESSOR
.
This will correspond to an array containing all the standard form control value accessors, plus our custom one.
When to use an useExisting provider
Notice also the use of the useExisting
option for creating a provider.
This option is useful when we want to create a provider based on another existing provider.
In this case, we want to define a provider in a simple way just by pointing to the ChooseQuantityComponent
class name, which can be used as a provider as we have learned.
This useExisting
functionality is also useful to define an alias (an alternative name) for an existing provider.
Now that we have a good understanding of how providers and injection tokens work, let's talk about another fundamental aspect of the Angular dependency injection system.
Understanding Angular Hierarchical Dependency Injection
Unlike its previous AngularJs version, the Angular dependency injection system is said to be hierarchical.
So what does that mean exactly?
If you notice, in Angular you have multiple places available where you can define the providers for your dependencies:
- at the level of a module
- at the level of a component
- or even at the level of a directive!
So what is the difference between defining a provider at all these different places, how does that work, and why are those different options even available?
You can define providers at multiple places because the Angular dependency injection system is hierarchical.
You see, if you need a dependency somewhere, for example if you need to inject a service into a component, Angular is going to first try to find a provider for that dependency in the list of providers of that component.
If Angular does not find a provider at the level of the component itself that needs the dependency, then Angular is going to try to find a provider in the parent component.
If it finds the provider it will instantiate and use it, but if not, it will ask the parent of the parent component if it has the provider that it needs, etc.
This process will be repeated until we reach the root component of the application.
If no provider is found after this process, you know what happens: yes, we get our good friend the "No provider found" message. 😉
This process of going up the component tree searching for the right provider is known as dependency resolution, and because it follows the hierarchical structure of our component tree, the Angular dependency system is said to be hierarchical.
We also need to know why this feature is useful.
What are the advantages of Hierarchical Dependency Injection?
Angular is typically used to build large applications, which in some cases can become quite large.
One way to manage this complexity is to split up the application into many small well encapsulated modules, that are by themselves split up into a well-defined tree of components.
These different sections of the page will need certain services and other dependencies to work, that they might or might not want to share with other sections of the application.
We can imagine a well isolated section of our page that works in a completely independent way than the rest of the application, with its own local copies of a series of services and other dependencies that it needs.
We want to ensure that these dependencies remain private and cannot be reached by the rest of the application, to avoid bugs and other maintenance issues.
Some of the services that our isolated section of the application uses might be shared with other parts of the application, or with parent components that are further up in the component tree, while other dependencies are meant to be private.
An hierarchical dependency injection system allows us to do just that! 👍
With hierarchical dependency injection, we can isolate sections of the application and give them their own private dependencies not shared with the rest of the application, we can have parent components share certain dependencies with its child components only but not with the rest of the component tree, etc.
An hierarchical dependency injection system allows us to build our system in a much more modular way, allowing us to share dependencies between different parts of the application only when and if we need to.
This initial explanation is a good starting point, but to really understand how this all works, we need a complete example.
Understanding Components Hierarchical Dependency Injection via an example
For example, let's take our CoursesService
class. What would happen if we would try to inject this class everywhere in the Angular component tree?
What would happen, would we get multiple instances of the service, or only one, how does that work?
To help us understand this, let's give to each instance of the CoursesService
a unique identifier, so that we can understand better what is going on:
Now let's create a simple hierarchy of components, inject CoursesService
in multiple places, and see what happens!
Let's create a root application component, that uses in its template a child
course-card
component.
Here is the root component app.component.html
template:
As we can see, this component internally uses the course-card
component inside an ngFor
loop.
Here is the component class app.component.ts
file:
Notice that we are adding CoursesService
to the providers of the root component, and we are also printing out to the console the unique identifier of the service instance that we get.
And finally, here is what the course-card
component looks like, this is the
course-card.component.ts
file:
Notice that we are here also adding CoursesService
to the list of providers of the component, as well as printing out to the console the id of the service instance.
If we now start our application, what do you think will happen?
How many instances of CoursesService
do you think will be created, and which ones get injected in which course-card
component?
Here is the output of the console:
App component service Id = 1
course card service Id = 2
course card service Id = 3
course card service Id = 4
course card service Id = 5
course card service Id = 6
course card service Id = 7
course card service Id = 8
course card service Id = 9
course card service Id = 10
course card service Id = 11
So what happened here? Let' break down what is going on.
So it looks like the application root component app.component.ts
was the first to create an instance of a service, so it got Id = 1.
The root component checked first its own list of providers when trying to get the CoursesService
dependency, and it found a matching provider and used it.
But what happened inside the course-card
component?
Unlike the application root component, the course-card
component has 10 instances created and displayed on the screen.
Each instance of the course-card
component needed a CoursesService
, so it tried to instantiate it by looking into its own list of providers.
Each course-card
component instance found a matching provider in its own providers private list, and used it to create a new instance of CoursesService
and inject in its dependencies.
This means that each course-card
instance created its own separate instance of CoursesService
, and did not need to ask the parent root component for an instance of CoursesService
.
There are 10 instances of course-card
, and each has its own private instance of CoursesService
, which explains the log!
Notice that these private instances of CoursesService
are linked to the component lifecycle of their course-card
instance.
So if the component instance of course-card
gets destroyed, their corresponding instance of CoursesService
will also get garbage collected.
But the CoursesService
is mostly a stateless class (except for our counter), so there is really no need to create so many instances of it.
What we would prefer, would be that there is only one instance of
CoursesService
, created by the root component, and shared with all its child components.
We can do so by removing the CoursesService
provider from the providers list of the CourseCardComponent
:
Notice the empty list of providers for this component, we can even remove the providers
property altogether.
If we do so and run our application again, here is what we get:
App component service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
As we can see, our application now only has one instance of CoursesService
, as intended. 👍
So we now understand how hierarchical dependency injection works in terms of the component tree only - Angular search for a provider match in the component first, and then scans all its parent components.
But what about modules? Those can also have their own providers, right?
Understanding Modules Hierarchical Dependency Injection via an example
Let's leave our components as they are currently, meaning:
- the app root component has a
CoursesService
provider - the
course-card
component does not have its own private providers
Notice that the course-card
component is part of a CoursesModule
.
So what happens if we keep the component providers as they are, and now add two more providers at the module level?
First let's add a provider at the level of CoursesModule
:
This is a feature module which is part of the application root module.
Let's now also add another provider at the level of the root module itself:
So we now have two extra providers in our application, and a total of 3 providers:
- 1 provider at the level of the root component
AppComponent
- 1 provider at the level of the feature module
CoursesModule
- 1 provider at the level of application root module
AppModule
If we now run our application, what do you think will happen?
Here is the console output:
App component service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
It's the exact same output!
So what is going on here?
Modules vs Components Dependency Injection Hierarchies
What is actually happening is that there are two separate dependency injection hierarchies:
- there is an hierarchy at the level of the components, that follows the tree structure of the components on the page
- But there is also a separate injection hierarchy at the level of the modules
And the hierarchy at the level of the components takes precedence over the module injection hierarchy! 😉
So when Angular is trying to find a dependency for a component or service, it will first try to create the dependency by using providers at the level of the component tree, if available.
If Angular follows all the components up until the root component without finding a matching provider and it still doesn't find anything, only then will Angular try to find a matching provider at the level of the modules hierarchy.
Angular will then start with the providers of the current module, and look for a matching provider.
If it doesn't find it, then Angular will try to find a provider with the parent module of the current module, etc. until reaching the root module of the application.
This system composed of two separate injection hierarchies allows us to modularize our application into two separate dimensions:
-
we can modularize our application by making certain dependencies available in a given section of the component tree by using the component injection hierarchy
-
but we can also modularize and create separate versions of a service that are used only by certain modules of our application by using the modules injection hierarchy
Now that we are familiar with the main concepts of how the hierarchical dependency injection system works, let's now learn how to further configure its dependency resolution mechanism.
Configuring the Dependency Injection Resolution mechanism
As we have learned, the Angular component dependency injection resolution mechanism always starts at the current component, and scans for matching providers all the way up the root component of the application, and it doesn't find a matching dependency it throws an error.
But what if we want to tweak this behavior a little bit?
Understanding the @Optional
decorator
For example, what if we would want to prevent the final error from being thrown, because the dependency might not be needed and you might have another alternative to using it?
We can mark a dependency as optional by using the @Optional
decorator:
This will prevent an error from being thrown if no matching provider is found for the dependency, but you need to make sure that your component checks if the dependency exists or not and provide an alternative to it if it doesn't.
Understanding the @SkipSelf
decorator
You can also tweak a bit the place where the dependency resolution mechanism starts looking for a matching provider.
For example, you might have some provider for a service at the level of your component that you want to provide to its child components, but the component itself needs an instance of that same service to work, and it needs to get it from its parent components and not from its own providers.
Admittedly, this must be a very rare case. 😃
But if you ever run into it, you can skip your local component providers and start the matching process directly at the parent component and from there down to the root component by using the SkipSelf
decorator:
In our application, this will result in the local CoursesService
provider to be skipped, meaning that that provider would only be visible to child components of the course-card
component.
If we run our application, this is the log that we would get:
App component service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
course card service Id = 1
As we can see, the provider at the level of CourseCardComponent
was skipped, and provider at the level of the CoursesModule
was used.
Understanding the @Self
decorator
Besides configuring where we start the match process for our dependencies, we can also tweak a bit where that matching process ends.
For example, if we want our component to look for a dependency in its own providers only, and skip checking the providers of all its parent components, we can do so by using the Self
decorator:
This time around, the local provider for CoursesService
is going to be taken, and the parent instance at the level of the CoursesModule
is going to be skipped.
Here is what our application log would look like in this case:
App component service Id = 1
course card service Id = 2
course card service Id = 3
course card service Id = 4
course card service Id = 5
course card service Id = 6
course card service Id = 7
course card service Id = 8
course card service Id = 9
course card service Id = 10
course card service Id = 11
As we can see, we fall again into the scenario where every instance of our component has its own private instance of a service.
Understanding the @Host
decorator
So far we have been talking about components and how they interact with the dependency injection system, but what about directives?
Because components are just a special type of directive, all that we have learned so far for components also applies to directives.
But there is one special case: imagine that the component has a private instance of a service, created by its own provider.
Now that component might or might not have a directive applied to it, that is part of the same module and that directive is designed so that it interacts closely with that particular component.
Because that directive is closely coupled with the component, it might want to access some of the private services associated to that component, and no other.
Imagine a simple HighlightedDirective
, that is meant to visually highlight a course card to which it's applied.
Let's say that this directive is designed to interact tightly with the
CourseCardComponent
, and would like to access its private instance of
CoursesService
.
The directive could access the private component service in the following way:
The use of the Host
decorator configures where Angular should stop searching for a dependency, in a similar way to what Self
does.
But in this case, this decorator is meant for directives only, and it says to Angular to search for a matching provider only on the providers of its host component, and nowhere else.
Let's now apply this directive to each course-card
component in our application:
Notice the use of the highlighted
attribute, that assigns a companion instance of the HighlightedDirective
to each instance of CourseCardComponent
.
If we now run our application, we get the following console output:
App component service Id = 1
course card service Id = 2
coursesService highlighted 2
course card service Id = 3
coursesService highlighted 3
course card service Id = 4
coursesService highlighted 4
course card service Id = 5
coursesService highlighted 5
course card service Id = 6
coursesService highlighted 6
course card service Id = 7
coursesService highlighted 7
course card service Id = 8
coursesService highlighted 8
course card service Id = 9
coursesService highlighted 9
course card service Id = 10
coursesService highlighted 10
course card service Id = 11
coursesService highlighted 11
As we can see, every instance of the HighlightedDirective
gets access to the private instance of the companion CoursesService
to which the directive is applied.
And with this, we have now fully covered the multiple ways that we have available to configure the Angular dependency resolution mechanism.
Now that we understand well how the hierarchical dependency injection works, let's talk about another of the many powerful features of the Angular dependency injection system.
What are Tree-Shakeable Providers?
The providers that we have been using so far as examples are missing one important property: they are not tree shakeable.
So what does this mean?
So far, the providers that we have been defining were all added explicitly to the list of providers of either a component or module, using the providers
property.
This approach has a practical problem though.
Imagine that an application has a dependency on a module that by its turn imports another module which then provides a service class.
Now imagine that this imported service class is effectively not used anywhere in our application, by whatever reason!
And this is actually a very common scenario.
If you are using for example AngularFire or other large third-party modules with tons of functionality, these modules contain all sorts of services that you might need in your application, or not.
You might use some of the services, but probably not all of them.
The idea is that we should be able to import a module, but if don't use some of its services then we shouldn't need to include them in our production bundle.
So how can this be achieved?
What is tree shaking?
When the Angular CLI is building our application bundle, it will try as much as possible to "tree shake" any code that is not necessary out of the bundle, in order to reduce its size as much as possible.
The CLI will try to do by statically inspecting the Typescript dependencies of our code, and determine if a dependency is being used or not.
If it finds that the dependency is not being used, it will remove it from the bundle and so reduce its size.
But if we are adding the module with the unused services directly in the providers property of a module or component, we need to first import that module or service directly via a Typescript import.
And this Typescript import will effectivelly prevent the tree shaking service from removing the unused service.
The tree shaker sees that the service is being imported explicitly via a Typescript import, and so it will wrongly think its being used and it won't remove it from the production bundle.
So how do we solve this?
Well, we need tree-shakeable providers. 😉
Understanding Tree-Shakeable Providers via an example
What we want to do is to define the CoursesService
as part of the CoursesModule
, which is a feature module.
This could perfectly be a third-party module that we imported in our application as well.
We want to define a provider for the CoursesService
in a way that, if it gets used by whoever imports the CoursesModule
, it will be included in the final bundle.
But if by some reason an application imports the CoursesModule
and ends up never using the CoursesService
, then the service should not get included in the bundle.
In order to do this, the first thing that we do is to remove the CoursesService
from the list of providers of the CoursesModule
:
But now, won't we get the "provider not found" error message?
Yes, because there is no provider defined yet. 😉
Here is how we can define a provider for CoursesService
at the level of the
CoursesModule
, without importing it in the courses.module.ts
file:
As you can see, we flipped the order of dependencies, we have imported the
CoursesModule
inside the CoursesService
and not vice-versa, and defined the module-level provider in the service class itself, using the Injectable
decorator.
This way, CoursesModule
does not import the service class at all, and so if the service is not used, it will be tree-shaked out of the production bundle as expected.
Notice that the Injectable
decorator allows us to define providers in all sorts of ways, and not only module-level providers.
We have also access to the options useClass
, useValue
, useExisting
, and deps
that we have access when configuring a provider at the level of a module or component.
Using providedIn
, we can not only define module-level providers, but we can even provide service to other modules, by making the services available at the root of the module dependency injection tree:
This is the most common syntax that you are probably already familiar with.
With this syntax, the CoursesService
is now an application-wide singleton, meaning that there is only instance of the service for the whole application, which makes sense in our case because our service is stateless.
And with this, we have reached the end of this section, let's now quickly summarize the key takeaways that we have learned throughout this post.
Summary
As we have learned, there is a lot going on behind the scenes with the Angular dependency injection system. 😉
The dependency system is very flexible, and it has a ton of powerful configuration options.
And yet, its mostly commonly used syntax of simply just dropping a class name into a list of providers is super easy to use!
Knowing how the dependency injection system works in detail will however prove to be very useful when you are designing your application in the most modular way possible.
The dependency injection system, because it's hierarchical and because it contains two separate injection hierarchies (components and modules), allows you to define in a very fine-grained way which dependencies are visible in which parts of the application, and which dependencies are hidden.
These features are very useful especially if you creating a third-party module and you want to ship services that might be used in a ton of different applications, but also if you are modularizing a large application.
Although all these powerful features are available, most of the time the default configuration mechanism just works and is super easy to use. 😃
I hope that you have enjoyed this post, if you would like to learn a lot more about many other powerful Angular core features, I recommend checking the Angular Core Deep Dive course, where dependency injection and many other features are covered in great detail.
Also, if you have some questions or comments please let me know in the comments below and I will get back to you.
To get notified of upcoming posts on Angular, I invite you to subscribe to our newsletter:
And if you are just getting started learning Angular, have a look at the Angular for Beginners Course: