Dependency injection in Angular
In Angular, dependencies refer to services and objects that a class requires to execute specific tasks. Dependency Injection (DI) is a coding pattern in which a class requests dependencies from an external source instead of creating them internally.
Now let’s discuss Dependency Injection (DI) in the context of an Angular application, where components, services, directives, and other types of objects/classes often depend on each other. Dependency injection (DI) in Angular provides a way to create and inject dependencies into components or services, making it easier to manage the relationships between different parts of our application.
To understand Dependency Injection (DI), let’s first discuss what a Provider and an Injector are.
Provider
A provider is an instruction to the Dependency Injection(DI) system on how to obtain a value for a dependency. A provider is a configuration that tells Angular Dependency Injection(DI) how to create and provide an instance of certain objects/dependencies. Most of the time, these dependencies are services that we create and provide.
@NgModule({
providers: [AnyService],
})
A provider implements the Provider
interfaces. A provider object defines how to obtain an injectable dependency associated with a DI token. An injector uses the provider to create a new instance of a dependency for a class that requires it.
We can register the dependency in providers:[]
metadata in 2 ways:
1. By directly registering the providers: []
of the NgModule or component or directive
@NgModule({
providers: [
MyService,
// other providers for this module
],
})
@Component({
selector: 'app-my-component',
template: '<p>Providers</p>',
providers: [
MyService,
// other providers for this component
],
2. We can also use providedIn
property of the @Injectable decorator. providedIn
syntax allows us to create what is known as a tree-shakable provider.
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root', // Provided at the application level as a singleton
// or specify a specific module: MyModule
})
export class MyService {
// service implementation
}
Injector
An injector is an object in the Angular Dependency Injection(DI) system that can find a named dependency in its cache or create a dependency using a configured provider.
1. An injector provides a singleton instance of a dependency and can inject this same instance in multiple components.
// logger.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root', // Provided at the application level as a singleton
})
export class LoggerService {
log(message: string): void {
console.log(message);
}
}
In this example, LoggerService
is marked with @Injectable({ providedIn: 'root' })
. As a result, Angular provides a single instance of LoggerService
for the entire application.
2. A hierarchy of injectors at the NgModule and component level can provide different instances of dependency on their own components and child components.
// module-b.module.ts
import { NgModule } from '@angular/core';
import { LoggerService } from './logger.service';
@NgModule({
providers: [
// ModuleB provides its own instance of LoggerService
LoggerService,
],
})
export class ModuleB { }
In the above example, ModuleB
provides its own instance of LoggerService
. All the components or services within ModuleB
will also share the same instance.
3. We can configure injectors with different providers that can provide different implementations of the same dependency.
providers: [
{ provide: LoggerService, useClass: CustomLoggerService },
],
// logger.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root', // Provided at the application level as a singleton
})
export class CustomLoggerService {
log(message: string): void {
console.warn("This log is coming from the custom logger Service", message);
}
}
Now the LoggerService
will refer to CustomLoggerService
. This demonstrates how we can configure different providers to supply different implementations of the same dependency in Angular through dependency injection, whenever we inject LoggerService
(which is now an instance of CustomLoggerService
) and use it to log a message.
In a Dependency Injection (DI) system, two main roles exist, the dependency consumer and the dependency provider. These roles help establish a clear separation of concerns and contribute to the decoupling of components within a software system.
Dependency Consumer:
- The dependency consumer is a component or class that depends on another service or object to perform certain tasks.
- It doesn’t create instances of its dependencies but relies on the Dependency Injection (DI) system to provide those instances.
- In Angular, components, services, and other objects often act as dependency consumers. They declare their dependencies in their constructors and rely on the Dependency Injection (DI) system to inject the appropriate instances.
Example (Angular component as a dependency consumer):
import { Component } from '@angular/core';
import { LoggerService } from './path-to-logger-service';
@Component({
selector: 'app-my-component',
template: '<button (click)="logMessage()">Log Message</button>',
})
export class MyComponent {
constructor(private logger: LoggerService) {}
logMessage(): void {
this.logger.log('Hello, Dependency Consumer!');
}
}
Dependency Provider:
- The dependency provider is responsible for creating and configuring instances of a particular service or object.
- In a Dependency Injection (DI) system, providers register the services they can create, allowing the Dependency Injection (DI) container to inject instances when requested.
- Providers can specify alternative implementations or configurations for services, allowing for flexibility and modularity.
Example (Angular module providing a service):
import { NgModule } from '@angular/core';
import { LoggerService } from './path-to-logger-service';
import { CustomLoggerService } from './path-to-custom-logger-service';
@NgModule({
providers: [
{ provide: LoggerService, useClass: CustomLoggerService },
],
// other module metadata...
})
export class MyModule { }
In the above examples, MyComponent
is a dependency consumer that relies on the LoggerService
. The MyModule
acts as a dependency provider by configuring the DI system to use CustomLoggerService
whenever an instance of LoggerService
is requested.
In summary, Dependency Injection(DI) is a mechanism for creating and delivering some parts of an application (dependencies) to other parts of an application that require them. This helps achieve a more modular and maintainable codebase. Components remain focused on their specific tasks without worrying about how to create or configure their dependencies, and the DI system takes care of providing the necessary instances.
Thanks for reading!!!