How Dependency Injection in Angular Works?

Monday, March 11, 2024

Angular has been one of the most popular open-source frameworks for front-end development. Angular app development companies are using this popular framework in creating modern-age web applications. The reason why it can deliver unique and robust applications is its key principle, angular dependency injection.

Dependency injection is a unique design pattern that enables developers to create scalable and efficient angular applications. In this blog, we will see how Angular dependency injection is used by developers to deliver top-notch applications. We will understand dependency injection along with its fundamental principles and implementation methods that every Angular app developer can use.

1. What is Dependency Injection?

Dependency Injection (DI) is an integral part of the Angular framework. It is a concept that offers the required components that can be used to access various services and resources that developers need to create applications. When it comes to Angular, it can inject a service into components which eventually offers access to the service to that particular component. In the dependency injection approach, the @Injectable() decorator specifies a class as a service and it enables the Angular app developers to inject this class into a component as a dependency. Similarly, this decorator also means that a class, pipe, component, or NgModule has a dependency on a service.

Angular dependency injection is an essential approach in Angular development because the injector is the main mechanism that enables the developers to create an application-wide injector. This injector then can create dependencies in the application and also maintain a container of all the instances that are reused in the application. Besides this, the provider is also a crucial part of this concept as it is an object that instructs an injector on how to create a dependency.

The Angular dependency injection system is an essential design pattern where the dependency injections of services or components are created.

2. Angular Dependency Injection: Fundamental Principles

Now, after having a brief understanding of dependency injection, let’s go through its core concepts that facilitate this approach to offer a smooth-running application. Here are some of the most important concepts of Angular dependency injections that can be used by developers while creating an application.

2.1 Providers

In dependency injection, Providers are known as objects that are liable to develop and manage instances of dependencies that developers can inject into services or components of the application. Providers can be defined at the application, module, or component level as required. Angular app development companies use Provider property to implement dependency in Angular. Here is an example that shows how developers can carry out the implementation process using a basic code.

// data.service.ts
import { Injectable } from '@angular/core';

@Injectable()
export class DataService {
  getItems = (): string[] => ['Item 1', 'Item 2', 'Item 3'];
}

// app.component.ts

import { Component } from '@angular/core';
import { DataService } from './data.service';

@Component({
  selector: 'app-root',
  template: `
    

Items List

  • {{ item }}
`, }) export class AppComponent { items: string[]; constructor(private dataService: DataService) { this.items = this.dataService.getItems(); } } // app.module.ts import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; import { DataService } from './data.service'; @NgModule({ declarations: [AppComponent], imports: [BrowserModule], providers: [DataService], bootstrap: [AppComponent], }) export class AppModule {}

In the above example, we saw that a class named DataService is defined with an @Injectable decorator. This class is injected by Angular as a dependency.

Besides, the provider comes with a few different properties that can be used to inject dependencies, and here are some of them.

useClass Property

The very first property of a Provider is useClass (e.g. FileLoggerService). It is an essential property as it helps the developers to specify the classes that are used as a dependency. Here is an example of the useClass property –

// logger.service.ts
import { Injectable } from '@angular/core';

@Injectable()
export class LoggerService {
  log(message: string): void {
    console.log(message);
  }
}


// file-logger.service.ts
import { Injectable } from '@angular/core';

@Injectable()
export class FileLoggerService {
  logToFile(message: string): void {
    // Implementation to log to a file
    console.log('Logging to file:', message);
  }
}


// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { LoggerService } from './logger.service';
import { FileLoggerService } from './file-logger.service';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule],
  providers: [{ provide: LoggerService, useClass: FileLoggerService }],
  bootstrap: [AppComponent],
})
export class AppModule {}

useValue Property

Another property is useValue, which is used by Angular developers to specify the value used as a dependency. Its implementation can be carried out as follows –

// app.module.ts


import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { DataService } from './data.service';

// Assuming you have a simple value or configuration object
const fileLoggerServiceConfig = {
  // your configuration properties here
};

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule],
  providers: [
    { provide: DataService, useValue: fileLoggerServiceConfig },
    // Use useValue to provide a constant value or configuration
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

// some.component.ts
import { Component, Inject } from '@angular/core';

@Component({
  selector: 'app-some',
  template: 'Config Value: {{ configValue }}',
})
export class SomeComponent {
  constructor(@Inject(DataService) private configValue: any) {}

  // Access configValue properties or use it as needed
}

The fileLoggerServiceConfig acts as a configuration object or constant value in the above example. Every time Angular encounters a dependency on DataService, we offer this value utilizing useValue in the providers’ array. Now, you can use any service or component of your app to inject the fileLoggerServiceConfig as a configuration object as per the above SomeComponent. 

useFactory Property

useFactory is a property that developers use to specify a factory function that can help in creating a dependency. Here’s an example of it –

// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { DataService } from './data.service';

// Assuming you have a simple value or configuration object
const fileLoggerServiceConfig = {
  // your configuration properties here
};

export function dataServiceFactory() {
  return fileLoggerServiceConfig;
}

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule],
  providers: [
    { provide: DataService, useFactory: dataServiceFactory },
    // Use useFactory to provide a custom factory function
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}


// some.component.ts
import { Component, Inject } from '@angular/core';

@Component({
  selector: 'app-some',
  template: 'Config Value: {{ configValue }}',
})
export class SomeComponent {
  constructor(@Inject(DataService) private configValue: any) {
    // Access configValue properties or use it as needed
  }
}

Here, we define dataServiceFactory, a factory function that returns fileLoggerServiceConfig. After that, using { provide: DataService, useFactory: dataServiceFactory } in the providers’ array, we tell Angular to use the factory function whenever it injects the DataService. This allows all your app’s services and components to inject DataService. Meanwhile, to render the fileLoggerServiceConfi, Angular uses the factory function. 

useExisting Property

The last type of Provider’s property is useExisiting. This property means that an existing dependency of the application must be used as a value of the new dependency. Here is an example of it –

// data.service.ts
import { Injectable } from '@angular/core';

@Injectable()
export class DataService {
  getItems = (): string[] => ['Item 1', 'Item 2', 'Item 3'];
}

// data-v2.service.ts
import { Injectable } from '@angular/core';

@Injectable()
export class DataV2Service {
  getItemsV2 = (): string[] => ['V2 - Item 1', 'V2 - Item 2', 'V2 - Item 3'];
}

// app.component.ts
import { Component } from '@angular/core';
import { DataService } from './data.service';
import { DataV2Service } from './data-v2.service';

@Component({
  selector: 'app-root',
  template: `
    

Items List

  • {{ item }}

V2 Items List

  • {{ item }}
`, providers: [ DataV2Service, { provide: 'DataServiceV2', useExisting: DataV2Service }, ], }) export class AppComponent { items: string[]; itemsV2: string[]; constructor(private dataService: DataService, private dataServiceV2: DataV2Service) { this.items = this.dataService.getItems(); this.itemsV2 = this.dataServiceV2.getItemsV2(); } } // app.module.ts import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; import { DataService } from './data.service'; @NgModule({ declarations: [AppComponent], imports: [BrowserModule], providers: [DataService], bootstrap: [AppComponent], }) export class AppModule {}

You can retrieve different sets of items using the getItemsV2 method from a newly introduced service called DataV2Service. By creating a new section in the AppComponent, you can display all the items retrieved from DataV2Service.

Now, create a new token named ‘DataServiceV2’ and use { provide: ‘DataServiceV2’, useExisting: DataV2Service } to offer DataV2Service under the new token. 

Use the AppComponent constructor to inject both DataV2Service and DataService. They can now help you populate item lists. In this way, you will be able to maintain the compatibility of the existing components that depend on the original service and still use useExisting to offer a different service instance. 

2.2 The Injector

Another important fundamental principle of Angular injection dependency is the Injector. It is a concept that enables the development and management of dependencies for Angular developers. There is a root injector for an entire Angular application that is created automatically and all the other injectors are then created by the developers are the children of the root injector. Here is an example that shows injector works –

// data.service.ts
import { Injectable } from '@angular/core';

@Injectable()
export class DataService {
  getItems = (): string[] => ['Item 1', 'Item 2', 'Item 3'];
}

// app.component.ts
import { Component, Injector } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    

Items List

  • {{ item }}
`, }) export class AppComponent { items: string[]; constructor(private injector: Injector) { // Use Injector to dynamically get the DataService const dataService = this.injector.get(DataService); this.items = dataService.getItems(); } }

The above code of the injector specifies a class named DataService which is determined with an @Injectable decorator. This means that the Angular developers can easily inject this class as a dependency. Another class named AppComponent defines a dependency on the Injector with the use of a constructor function. After that, the data property of the class is set to the result to call the method named getItems() on the DataService instance that can retrieve data.

3. Implementing Dependency Injection in Angular

Now, let’s have a look at the implementation method of the dependency injection in the Angular framework.

3.1 Providing Dependency

The very first step in the implementation method is to provide dependency. For this, let’s go through an example that gives a clear idea of the process. For instance, there is a class named   DataService that is required to act as a component’s dependency. In this case, the developers need to first add the @Injectable decorator as it will help in showcasing the class that is injected in the process.

// data.service.ts
import { Injectable } from '@angular/core';

@Injectable()
export class DataService {
  getItems = (): string[] => ['Item 1', 'Item 2', 'Item 3'];
}

After that, the class must be made available in the dependency injection for which there are various places. Here we will see where the dependency can be provided into a system.

  • Component level: Here, the developers can use the provider’s field of the @Component decorator which will help in making the DataService class available to all the instances of the specific component along with others and directives. For this, the following code must be written –
// app.component.ts

import { Component } from '@angular/core';
import { DataService } from './data.service';

@Component({
  selector: 'app-root',
  template: `
    

Items List

  • {{ item }}
`, providers: [DataService] }) export class AppComponent {}

As seen in the above code, a new instance of that particular service will be created when the developed registers a provider in the component injector.

  • NgModule level: Here, the Angular developers can use the provider’s field of the @NgModule decorator. This is a case that makes the service class available for all the pipes, directives, and components that are declared at the NgModule level within the same injector. The developer needs to register a provider with the NgModule which will make the same instance of that particular service available to all the components, pipes, and directives. For this, the below code can be helpful.
@NgModule({
  declarations: [AppComponent]
  providers: [DataService]
})
class AppModule {}
  • Application root level: This is a place where the developers provide dependency by injecting it into various other classes of the application. For this the providedIn: ‘root’ field is added to the @Injectable decorator as shown in the below code.
// data.service.ts
import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class DataService {
  getItems = (): string[] => ['Item 1', 'Item 2', 'Item 3'];
}

3.2 Injecting a Dependency

After providing the dependency, it’s time to inject it. There is a common method that every Angular developer can follow to inject a dependency and that is to declare the dependency in a class constructor. While working on a project, when Angular creates a new instance of Angular pipes, components, or directive class in an Application, it specifies all the services that a class will require after looking at the types of the constructor parameter of that class. For instance, if the DataService class is required by the AppComponent,  the constructor for this situation will be –

@Component({ ... })
class HeroListComponent {
  private service = inject(HeroService);
}


@Component({ ... })
class AppComponent {
  private service = inject(DataService);
}

In this case, when the Angular system sees that a component instance is dependent on a particular service, it will check the injector to see if it has any existing instances. If there is no service instance, the injector will create one and add it before the service returns to Angular.

4. Take Away

As seen in this blog, Angular Dependency Injection is very helpful to developers who are creating complex applications as they can make use of the dependency providers and injectors to get instances of the services as per the end-users requirements. This makes the applications run faster and deliver a robust result. This is why nowadays, the majority of Angular app development companies are using the dependency injection concept to take their development to the next level.

FAQ

What is Angular Dependency Injection?

Angular dependency injection (DI) is a special mechanism and a design pattern that enables the Angular developers to create and deliver some modules of the application to another one as per the requirement. 

How to Use inject () in Angular?

When it comes to inject() in Angular, it is used in the construction process with the use of a constructor. It works inside the injection context and is called when an instance in an application is created. 

Comments


Your comment is awaiting moderation.