Angular Dependency Injection: In-depth

jagadeeshmusali

Jagadeesh Musali

Posted on February 20, 2022

Angular Dependency Injection: In-depth

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
   }
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
...
}
Enter fullscreen mode Exit fullscreen mode

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) {
    ...
  }
  ...
}
Enter fullscreen mode Exit fullscreen mode

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 to ItemsComponent. So we get a famous error!

  NullInjectorError: No provider for ItemsService!
Enter fullscreen mode Exit fullscreen mode

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(ItemsServicein 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);
}
Enter fullscreen mode Exit fullscreen mode

Like I said earlier, this is just a plain function that takes as input any dependencies that ItemsServiceneeds. 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");
Enter fullscreen mode Exit fullscreen mode

You can actually use any unique value but to save you from that effort Angular added InjectionTokenclass, 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 { }
Enter fullscreen mode Exit fullscreen mode

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 ItemsServicedependency.

We can do this just by using @Inject annotation where-ever ItemsServiceis 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) {
    ...
  }
  ...
}
Enter fullscreen mode Exit fullscreen mode

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]
    }
    ]
Enter fullscreen mode Exit fullscreen mode

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 { }
Enter fullscreen mode Exit fullscreen mode

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) {
    ...
  }
  ...
}
Enter fullscreen mode Exit fullscreen mode

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 useClassproperty.

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 { }
Enter fullscreen mode Exit fullscreen mode

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) {
    ...
  }
  ...
}
Enter fullscreen mode Exit fullscreen mode

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 { }
Enter fullscreen mode Exit fullscreen mode

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;
   }
...
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
...
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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}`);
  }
...
}
Enter fullscreen mode Exit fullscreen mode

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}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

As you see, unlike the app component, Item component looks like getting new instance of ItemsService every time.

Each instance of the ItemComponent needed a ItemsService, 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 ItemsServiceinstance.

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}`);
  }
...
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 ItemsModulein AppModule. But for some reason, in the AppModule we ended up never using ItemsService. In this case we dont want to include ItemsServicein the final bundle.

In order to achieve this we have to remove the ItemsServicereference from the list of providers of the ItemsModule.

@NgModule({
  imports: [ ... ]
})
export class ItemsModule { }
Enter fullscreen mode Exit fullscreen mode

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 providerwithout importing it in the modulefile.

items.service.ts

@Injectable({
  providedIn: ItemsModule
})
export class ItemsService() {

  constructor(private http: HttpClient) {

  }
...
}
Enter fullscreen mode Exit fullscreen mode

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) {

  }
...
}
Enter fullscreen mode Exit fullscreen mode

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.

๐Ÿ’– ๐Ÿ’ช ๐Ÿ™… ๐Ÿšฉ
jagadeeshmusali
Jagadeesh Musali

Posted on February 20, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

ยฉ TheLazy.dev

About