Angular DI Container: How does it work?

Photo by C Dustin on Unsplash

Angular DI Container: How does it work?

What is Depdency Injection?

Dependency injection is basically providing the objects that an object needs (its dependencies) instead of having it construct them itself.

An analogy to understand dependency injection is constructing a home. When building a home, we don't make every component from scratch. For example, instead of constructing a door ourselves, we rely on a specialized door supplier to provide a pre-built door that fits our requirements. The door is a dependency of the home construction process, and we inject it into the home.

Similarly, in software development, we identify the dependencies of a class or module and inject them from the outside. This allows for more flexibility, easier testing, and better separation of concerns.

// Door class
class Door {
  constructor(color) {
    this.color = color;
  }

  open() {
    console.log(`The ${this.color} door is opened.`);
  }
}

// House class with dependency injection
class House {
  constructor(door) {
    this.door = door;
  }

  enter() {
    console.log("You entered the house.");
    this.door.open();
  }
}

// Creating instances and injecting dependencies
const door = new Door("red"); // Dependency
const house = new House(door); // Dependency injection

// Entering the house
house.enter();

One problem that can arise with dependency injection is the increased complexity and potential for managing a large number of dependencies within a class. If a class depends on a significant number of objects (e.g., 10, 20, or more), it can become cumbersome to manually provide and manage all the dependencies.

To address this issue, Dependency Injection (DI) Containers, like the one provided by Angular, can help simplify the management of dependencies in a more automated and centralized manner.

DI Container

A DI container is a tool used to manage dependencies in a software application. It provides a mechanism to register dependencies and, when requested, creates and returns instances of those dependencies*. The DI container acts as a central hub for dependency management, facilitating the retrieval and instantiation of dependencies throughout the application.*

Angular DI Container

In Angular, the Dependency Injection (DI) system is an integral part of the framework and is powered by Angular's built-in DI container.

To use the Angular DI container, you can follow these steps:

Step 1: Register the Dependency wiht a Token and Provide the Resolution Configuration

In this step, we register the dependency and provide the necessary configuration for resolving and instantiating the dependency.

To register a depdency Angular DI Container expects two things: a Token and the Depdency Resolution Configuration

Option 1: Using @Injectable()
import { Injectable } from '@angular/core';

@Injectable()
export class DoorService {
  // ...
}

In this example, we register the DoorService class dependency using the @Injectable() decorator. By marking the class with @Injectable(), Angular treats the class itself as a token for registration and resolution. When the DoorService dependency is requested, Angular's DI system automatically resolves and provides an instance of DoorService. This approach is suitable when the token (i.e., class name) itself can be used for registration and resolution.

Option 2: Using the providers Array

  1. Class Providers:
import { NgModule } from '@angular/core';
import { MyService } from './my-service';

@NgModule({
  providers: [
    DoorService
    // ... other dependencies
  ]
})
export class DoorService {
  // ...
}

In this example, we register the DoorService class dependency using the providers array within the @NgModule decorator. By including DoorService in the providers array, we provide the configuration for resolving and instantiating the dependency. When the DoorService dependency is requested within the module or its child components, Angular's DI system resolves and provides an instance of DoorService based on the provided configuration.

Note: When registering a class as a dependency, if the token (class name) itself is used for registration, Angular automatically resolves the dependency based on the class. In such cases, there's no need to explicitly provide a useClass configuration in the provider array.

  1. Value Providers:
    When registering a class as a dependency, Angular automatically resolves and instantiates it. No additional configuration is required. Here's an example:
     import { NgModule } from '@angular/core';
    
     @NgModule({
       providers: [{ provide: 'API_KEY', useValue: 234234 }]
     })
     export class MyModule {
       // ...
     }
    

    In this example, Inside the providers array of the NgModule decorator, we provide the 'API_KEY' token using the useValue property and assign it the value of API_KEY. This configuration allows Angular's DI system to resolve the 'API_KEY' token and provide it with the value '234234' when requested.

  2. Using useFactory Provider:

    The useFactory provider allows us to register a dependency by providing a factory function that creates and returns an instance of the dependency.

     import { NgModule } from '@angular/core';
    
     @NgModule({
       providers: [
         {
           provide: 'DoorService',
           useFactory: () => ({ color: 'red', ... })
         }
       ]
     })
     export class MyModule {
       // ...
     }
    

    In this example, we define a factory function using the useFactory property. The factory function does not have any parameters and returns an object with properties, such as color: 'red'. The factory function is assigned as the provider for the 'DoorService' token using the provide property.

    When the 'DoorService' dependency is requested, Angular's DI system calls the factory function and provides the returned object as the instance of the 'DoorService' dependency. In this case, the returned object will have the properties defined in the factory function, such as color: 'red'.

  3. Using UseExisting Provider:

    The useExisting provider allows us to register a dependency using an alias for another provider.

     import { NgModule } from '@angular/core';
    
     @NgModule({
       providers: [
         {
           provide: 'AliasDependency',
           useClass: DoorService
         }
       ]
     })
     export class MyModule {
       // ...
     }
    

    In this modified example, we have replaced the MyDependency class with DoorService. Now, when the 'AliasDependency' dependency is requested, Angular's DI system resolves it to the DoorService class and provides an instance of DoorService.

Step 2: Request Depdency

When you have a dependency registered with the same token as the class name, Angular's DI system can automatically inject the dependency for you. However, if the token name is different, such as when using value, factory, or alias providers, you need to explicitly specify the dependency using the @Inject() decorator.

Example 1: Dependency with the Same Token

@Injectable()
export class DoorService {
  // ...
}

@Component({
  // ...
})
export class MyComponent {
  constructor(private doorService: DoorService) {
    // doorService is automatically injected
    // ...
  }
}

Example 2: Dependency with a Different Token

@Injectable()
export class DoorService {
  // ...
}

@Component({
  // ...
})
export class MyComponent {
  constructor(@Inject('AliasDependency') private doorService: DoorService) {
    // doorService is explicitly requested using the 'AliasDependency' token
    // ...
  }
}

By using the @Inject() decorator, you can specify the token for dependencies that are registered with different tokens, ensuring that Angular's DI system resolves and provides the correct dependency instance for your component or service.

Did you find this article valuable?

Support Sujeet Agrahari by becoming a sponsor. Any amount is appreciated!