Introduction
Angular has its build-in dependency injection system with most powerful and standout features.
Most of us might have already been using this feature and don't even realize it, thanks to its implementation by Angular team. But understanding this system in depth would help us from architectural perspective along with below:
Solve some weird dependency injection errors
Build applications in more modular way
Configure some third party libraries with customizable services
Implementing tree-shakable features
Dependency Injection
So what exactly is this dependency injection?
It is a technique in which an Object receives other Objects that it depends on, and is often used pretty much everywhere in modern applications irrespective of tech stack.
So for example, you write a service that gets data from backend, you would certainly need an HTTP service to request and there could be other similar dependencies.
So you might end up creating your own dependencies in the service, like shown below:
items.service.ts:
export class ItemsService() {
http: HttpClient;
constructor() {
this.http = new HttpClient(httpHandler);
}
}
At first this might look like a straight forward solution, but as the time goes on, it would be very hard to maintain and test.
Notice that this class knows not only how to create its dependencies, but it also knows about the dependencies of the
HTTPClient
class as well.
Try to compare this that leverages angular dependency injection:
items.service.ts:
@Injectable()
export class ItemsService() {
http: HttpClient;
constructor(http: HttpClient) {
this.http = http;
}
}
This version, does not need to create its dependencies, it just receives those as input arguments from constructor.
Now lets try to setup dependency injection step by step deep dive into it. Hands on time!
Lets start this by creating a simple class:
items.service.ts:
export class ItemsService() {
http: HttpClient;
constructor(http: HttpClient) {
this.http = http;
}
...
}
Now lets try to use this class in different part of application. For our understanding, lets assume we are using this in ItemsComponent
:
export class ItemsComponent {
constructor(private ItemsService: ItemsService) {
...
}
...
}
Notice that this class is not yet linked to Angular dependency injection system, so Angular does not know at this point a way to get
ItemsService
instance in order to provide toItemsComponent
. So we get a famous error!
NullInjectorError: No provider for ItemsService!
Now, if you inspect this error, you understand that Angular is looking for something known as provider
.
Dependency Injection Providers
The error No provider
means simply that Angular cannot initialize a given dependency(ItemsService
in our case) and provide to ItemsComponent
because it does not know how to create it.
To do that, Angular needs to know what is known as a provider factory function.
Do not get tripped by fancy names, end of the day, it is nothing but a simple function that returns an instance of your dependency(ItemsService
in our case).
The point to understand here is that for every single dependency in your application, be it a service or a component, there is somewhere a plain function that is being called and that function returns your dependency instance.
This provider factory function can be automatically created by Angular for us with some simple conventions. But we can also provide that function ourselves.
Lets create one ourselves to really understand what a provider is.
Create our own provider factory function
function ItemsServiceProvider(http:HttpClient): ItemsService {
return new ItemsService(http);
}
Like I said earlier, this is just a plain function that takes as input any dependencies that ItemsService
needs. And it will then returns ItemsService
instance.
Now if you think on highlevel of what we have done, we have got a service thats being used in a component. And we have created a provider factory function that returns instance of ItemsService
.
So at this point, Angular yet to understand that we wrote this provider factory function(item 1).
More important than that, we need to tell Angular that it needs to use this function whenever it has to provide
ItemsService
instance(item 2).
So lets dive in to Injection Tokens to make a link between provider factory function that we wrote and ItemsService
.
Item 1
Injection Tokens
In order to uniquely identify dependencies, we can define what is know as an Angular Injection Token. Again, dont get tripped by fancy names, its just an unique identifier for Angular to link our ItemsService
.
export const ITEMS_SERVICE_TOKEN =
new InjectionToken<ItemsService>("ITEMS_SERVICE_TOKEN");
You can actually use any unique value but to save you from that effort Angular added InjectionToken
class, It just returns an unique token Object.
Now that we have an unique object to identify dependencies, how do we use it?
Manually configure a provider
Now that we have provider factory function and injection token, we can configure a provider in the Angular dependency injection system, that will know how to create instances of ItemsService
if needed.
We can do this in module as shown below:
items.module.ts:
@NgModule({
imports: [ ... ],
providers: [
{
provide: ITEMS_SERVICE_TOKEN,
useFactory: itemsServiceProvider,
deps: [HttpClient]
}
]
})
export class ItemsModule { }
As you see, manually configured provider needs
provide
: An injection token of dependency type(ItemsService in our case) helps Angular determine when a given provider factory function should be called or not.useFactory
: provider factory function. Angular will call when needed to create dependencies and inject them.deps
: dependencies that are needed for provider factory function. HTTP client in our case.
Item 2
So now Angular knows how to create instances of ItemsService
right? But if you run app, you might be surprised to see that same NullInjectorError
Well, that is because we are yet to tell Angular that it should use our items provider to create ItemsService
dependency.
We can do this just by using @Inject
annotation where-ever ItemsService
is being injected.
items.component.ts:
@Component({
selector: 'Items',
templateUrl: './Items.component.html',
styleUrls: ['./Items.component.css']
})
export class ItemsComponent {
constructor( @Inject(ITEMS_SERVICE_TOKEN) private itemsService: ItemsService) {
...
}
...
}
Ok, we have done a lot so far, lets recap whats happening at this point.
The explicit use of @inject
decorator allows us to tell Angular that in order to create this dependency, it needs to use the specific provider linked to the ITEMS_SERVICE_TOKEN
injection token.
This token contains unique information that identifies a dependency type from the point of view of Angular, and this way Angular knows that it needs to use ItemsService Provider factory function.
It goes ahead and does just that. With this our application should now work correctly with no more errors!
Great, but have you ever thought:
Why don't I usually configure providers?
You are right, you usually dont have to do all this to configure provider factory functions or injection tokens yourself. Angular takes case of all that for you in the background.
Angular always automatically creates a provider and an injection token for us under the hood
To better understand this, let's simplify our provider and after few iterations, you would reach to something that you are much more used to.
Iteration 1
Lets see the providers that we have.
providers: [
{
provide: ITEMS_SERVICE_TOKEN,
useFactory: itemsServiceProvider,
deps: [HttpClient]
}
]
As we discussed earlier, injecton token is just for Angular to uniquely identify. Just like an ID field in database. So you can simplify this by just using class name.
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.
@NgModule({
imports: [ ... ],
providers: [
{
provide: ItemsService,
useFactory: itemsServiceProvider,
deps: [HttpClient]
}
]
})
export class ItemsModule { }
As you can see, we are no longer using ITEMS_SERVICE_TOKEN injection token to uniquely identify dependency type. And using the class name itself to identify the dependency type.
If you run your app, you would get an error. Well if you think about it, we have removed ITEMS_SERVICE_TOKEN here, but we are yet to change it in the ItemsComponent
items.component.ts:
export class ItemsComponent {
constructor( @Inject(ItemsService) private itemsService: ItemsService) {
...
}
...
}
And with this, everything should start working again.
Iteration 2
We wrote a provider factory function with useFactory
. Instead of that we have other ways to tell Angular on how to instantiate a dependency.
We can use useClass
property.
This way, Angular will know that the value we are passing is a valid class. So basically Angular just simply calls it using new
operator.
items.module.ts:
@NgModule({
imports: [ ... ],
providers: [
{
provide: ItemsService,
useClass: ItemsService,
deps: [HttpClient]
}
]
})
export class ItemsModule { }
Now this greatly simplifies our provider as we do not need to write a provider factory function manually.
Iteration 3
Along with this convenient feature of userClass
, Angular will try to infer the injection token at runtime. So we dont even need the inject
decorator in the component anymore.
items.component.ts:
export class ITemsComponent {
constructor(private itemsService: ItemsService) {
...
}
...
}
Iteration 4
Instead of defining a provider object manually in modules, we can simply pass the name of class.
items.module.ts:
@NgModule({
imports: [ ... ],
providers: [
ItemsService
]
})
export class ItemsModule { }
With this Angular under hood will infer that this provider is a class, it will then create a factory function and create a instance of the class on demand.
All this happens just by the class name. This is what you would see more often which is super simple and easy to use.
As you can see, Angular made this super simple and you won't even realize that there are providers and injection tokens stuff happening behind the scenes.
Iteration 5
Remember the deps
property? Now that we have removed our manually created provider object with just class name, Angular will not know how to find the dependencies of this class.
To let Angular know, you also need to apply Injectable() decorator to the service class
@Injectable()
export class ItemsService() {
http: HttpClient;
constructor(http: HttpClient) {
this.http = http;
}
...
}
This decorator will tell Angular to find the dependencies for this class at runtime!
Generally this is how we use the Angular Dependency Injection system without even worrying about all the stuff that is happening under the hood.
Angular Dependency Injection Hierarchy
So far we have defined our providers in modules
ie ItemsModule
in our case. But these providers can be defined in other places as well.
At component
At directive
At module(we have already done that)
So lets see the differences between defining a provider at each place and its impact.
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 doest find it there, then Angular is going to try to find provider in parent component. If it finds, it will instantiate and use it. But if not, it will check parent of parent component and so on.
If it still doesnt find anything, 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.
If no provider is found even at root module, we get our famous error No provider found
This hierarchical dependency injection system allows us to build our applications in a much more modular way. With this we can isolate different parts of out application and can make them to interact only if needed.
There are some implications to this if you don't understand how all this works.
Lets get to hands on again! to see what am talking about.
Hands on for Hierarchical Dependency
Lets add some unique identifier inside our service.
items.service.ts:
let counter = 0;
@Injectable()
export class ItemsService() {
constructor(private http: HttpClient) {
counter++;
this.id = counter;
}
...
}
Now lets create a simple parent child components and inject ItemsServcice
in multiple paces and see what happens.
Parent component is AppComponent
Child component is ItemComponent
So assume we need to display list of all items. Below is the template
app.component.html:
<div>
<item *ngFor="let item of items"
[item]="item">
</item>
</div>
Here is the component class
app.component.ts:
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
providers: [ ItemsService ]
})
export class AppComponent {
constructor(private itemsService: ItemsService) {
console.log(`App component service Id = ${itemsService.id}`);
}
...
}
Notice that we are adding ItemsService to the providers of the AppComponent
Lastly, below is our ItemComponent
Item.Component.ts:
@Component({
selector: 'item',
templateUrl: './item.component.html',
styleUrls: ['./item.component.css'],
providers: [ ItemsService ]
})
export class ItemComponent {
constructor(private itemsService: ItemsService) {
console.log(`items service Id = ${itemsService.id}`);
}
}
Notice that we are adding ItemsService to the providers of this component as well.
Now, lets run our application and assume we have 3 items and guess what happens?
App component service Id = 1
items service Id = 2
items service Id = 3
items service Id = 4
As you see, unlike the app component
, Item component
looks like getting new instance of ItemsService
every time.
Each instance of the
ItemComponent
needed aItemsService
, it tried to intantiate it by looking at its own list of providers. Each of item component instance found a matching provider in its own providers list. So it uses it to create a new instance each time.
In most of the applications, the services tent to be written stateless. So there is really no need to create so many instances of it. Just one instance should be good enough.
So if we just remove providers in this component, Angular will reach to parent component for ItemsService
instance.
item.component.ts:
@Component({
selector: 'item',
templateUrl: './item.component.html',
styleUrls: ['./item.component.css']
})
export class ItemComponent {
constructor(private itemsService: ItemsService) {
console.log(`items service Id = ${itemsService.id}`);
}
...
}
Now if we run our application you should just see one instance of the service
App component service Id = 1
items service Id = 1
items service Id = 1
items service Id = 1
Tree-Shakeable Providers
To explain in simple terms, Including only necessary code in the final bundle and removing un-used code is called tree shaking.
Assume we have two modules, a root module(AppModule
) and a feature module(ItemsModule
). Our ItemsService
would be defined under ItemsModule
.
So if we need to use some component from ItemsModule
, we would have to import ItemsModule
in AppModule
. But for some reason, in the AppModule
we ended up never using ItemsService
. In this case we dont want to include ItemsService
in the final bundle.
In order to achieve this we have to remove the ItemsService
reference from the list of providers of the ItemsModule
.
@NgModule({
imports: [ ... ]
})
export class ItemsModule { }
But then if we do that, dont we get "provider not found" error?
Yes we would. because there is no provider. Here is how we can define a provider
without importing it in the module
file.
items.service.ts
@Injectable({
providedIn: ItemsModule
})
export class ItemsService() {
constructor(private http: HttpClient) {
}
...
}
As you can see we just flipped the order of dependencies. This way ItemsModule
will not import ItemsService
.
So if anyone who is importing ItemsModule
, and is not using ItemsService
, code related to ItemsService will not be included in the final bundle.
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.
items.service.ts:
@Injectable({
providedIn: "root"
})
export class ItemsService() {
constructor(private http: HttpClient) {
}
...
}
This is the most common syntax that you would be seeing in day to day of Angular developer.
With this syntax, the ItemsService
is now an application wide singleton, meaning there would always be only one instance of it.
Conclusion
There is lot going on behind the scenes that is making things easier for us related to dependency injection. But knowing how the system works in detail will be helpful in making key decisions in you applications architecture. Thanks for reading. If you have some questions, corrections that I might have to make in the article or comments please let me know in the comments below and I will get back to you.
Top comments (0)