Dependecy Injection and Providers in Angular
3 min readAug 12, 2024
Angular’s Dependency Injection (DI) system is highly configurable and can be extended or customized in several ways to suit different application needs. Here’s a breakdown of how you can customize Angular’s DI system:
1. Providing Services in Different Scopes
- Root-Level Providers: By using
providedIn: 'root'
, Angular creates a singleton instance of the service that is shared across the entire application.
@Injectable({ providedIn: 'root', })
export class ExampleService {
// Singleton service across the entire app
}
- Module-Level Providers: If you want a service to be specific to a module, you can provide it in that module. This is particularly useful for lazy-loaded modules.
@NgModule({
providers: [ExampleService], // Service is scoped to this module
})
export class ExampleModule {}
- Component-Level Providers: You can also provide a service at the component level, creating a new instance of the service for that component and its children.
@Component({
selector: 'app-child',
templateUrl: './child.component.html',
providers: [ExampleService], // New instance for this component and its children
})
export class ChildComponent {}
2. Custom Providers
Angular allows you to define custom providers that determine how and when services are created.
- Use Class: You can tell Angular to use a specific class to provide a service.
@Injectable()
export class CustomService {}
@NgModule({
providers: [
{ provide: ExampleService, useClass: CustomService }
],
})
export class AppModule {}
- Use Existing: This option allows you to reuse an existing service.
@NgModule({
providers: [
{ provide: ExampleService, useClass: CustomService },
{ provide: AnotherService, useExisting: ExampleService },
],
})
export class AppModule {}
- Use Factory: This allows you to use a factory function to create the service.
export function exampleFactory() {
return new ExampleService();
}
@NgModule({
providers: [{ provide: ExampleService, useFactory: exampleFactory }],
})
export class AppModule {}
- Use Value: You can provide a simple value using the
useValue
option. This is often used for configuration values
@NgModule({
providers: [{ provide: 'API_URL', useValue: 'https://api.example.com' }],
})
export class AppModule {}
3. Multi-Providers
- Multi-Providers: Angular allows you to provide multiple values for a single token using
multi: true
. This is useful for things like custom validators, interceptors, or event handlers.
export const CUSTOM_HANDLERS = new InjectionToken<string[]>('CustomHandlers');
@NgModule({
providers: [
{ provide: CUSTOM_HANDLERS, useValue: 'handler1', multi: true },
{ provide: CUSTOM_HANDLERS, useValue: 'handler2', multi: true },
],
})
export class AppModule {}
- Injecting Multi-Providers: When you inject a multi-provider, you receive an array of all the provided values.
constructor(@Inject(CUSTOM_HANDLERS) private handlers: string[]) {
console.log(this.handlers); // ['handler1', 'handler2']
}
4. Injection Tokens
- Creating Custom Injection Tokens: Use
InjectionToken
to create custom tokens for dependency injection, especially when you need to inject something that isn't a class, like a string or a configuration object.
export const API_URL = new InjectionToken<string>('apiUrl');
@NgModule({
providers: [{ provide: API_URL, useValue: 'https://api.example.com' }],
})
export class AppModule {}
// Injecting the custom token
constructor(@Inject(API_URL) private apiUrl: string) {}
5. Hierarchical Dependency Injection
- Different Instances Based on Injector Hierarchy: Angular’s DI system is hierarchical, which means that depending on where a service is provided, different instances of that service may be created.
- Parent and Child Injectors: Angular creates a tree of injectors, with each component having its own injector. If a service is not found in a child injector, Angular will search up the injector tree until it finds one that can provide the service.
- Lazy-Loaded Modules and Providers: When a module is lazy-loaded, Angular creates a separate injector for that module. Services provided in a lazy-loaded module will be unique to that module, and won’t affect the rest of the app.
6. Optional Dependencies
- Optional Decorator: You can mark a dependency as optional using the
@Optional
decorator. If Angular can't resolve the dependency, it will injectnull
instead of throwing an error.
constructor(@Optional() private optionalService: OptionalService) {
if (this.optionalService) {
// Use the service
} else {
// Handle the absence of the service
}
}
7. Forward References
- ForwardRef: If you have circular dependencies, where two classes depend on each other, you can use
forwardRef
to resolve this.
import { forwardRef, Inject } from '@angular/core';
export class AService {
constructor(@Inject(forwardRef(() => BService)) private bService: BService) {}
}
export class BService {
constructor(private aService: AService) {}
}
Conclusion
Angular’s DI system is powerful and flexible, allowing you to manage dependencies in a variety of ways. By understanding and utilizing custom providers, hierarchical injection, multi-providers, and other advanced features, you can build more complex and scalable applications.
Thanks for reading.