Angular DI
Angular Dependency Injection
Section titled “Angular Dependency Injection”A design pattern where objects receive their dependencies from an external source rather than creating them internally.
In Angular,
- DI is a framework that provides dependencies to classes rather than having classes create dependencies themselves.
- It provides loose coupling benefits.
Without DI:
class Car { private engine: Engine;
constructor() { this.engine = new Engine(); // Tight coupling }}With DI:
class Car { constructor(private engine: Engine) {} // Loose coupling}Why Use DI in Angular?
Section titled “Why Use DI in Angular?”Benefits and advantages of using Dependency Injection in Angular applications
- Testability: Easy to mock dependencies in tests
- Maintainability: Changes to dependencies don’t affect consumers
- Reusability: Components can be easily reused with different configurations
- Loose Coupling: Classes don’t need to know how to create dependencies
- Centralized Configuration: Dependency configuration in one place
DI Core Concepts
Section titled “DI Core Concepts”Fundamental building blocks of Angular’s Dependency Injection system
1. Injector
Section titled “1. Injector”The container that holds dependencies and provides them when requested
// Angular creates injectors automaticallyconstructor(private injector: Injector) {}2. Provider
Section titled “2. Provider”Tells Angular how to create or deliver a dependency
// Providers tell Angular what to inject and howproviders: [MyService, { provide: API_URL, useValue: "https://api.com" }];3. Dependency
Section titled “3. Dependency”A class, value, or factory that another class needs to function
// Logger is a dependency of MyServiceconstructor(private logger: Logger) {}Provider Types
Section titled “Provider Types”Different ways to configure how dependencies are provided
1. Class Provider
Section titled “1. Class Provider”Provides an instance of a class (most common)
providers: [MyService];// Equivalent to:providers: [{ provide: MyService, useClass: MyService }];2. Value Provider
Section titled “2. Value Provider”Provides a specific value (constants, config objects)
providers: [ { provide: API_URL, useValue: "https://api.example.com" }, { provide: APP_CONFIG, useValue: { theme: "dark", version: "1.0" } },];3. Factory Provider
Section titled “3. Factory Provider”Uses a factory function to create the dependency
providers: [ { provide: MyService, useFactory: (config: Config) => { return config.production ? new ProductionService() : new DevelopmentService(); }, deps: [Config], },];4. Aliased Provider
Section titled “4. Aliased Provider”Provides an existing token under a new token
providers: [ OldService, { provide: NewService, useExisting: OldService }, // Alias];5. Optional Provider
Section titled “5. Optional Provider”Makes a dependency optional (won’t throw error if not found)
constructor(@Optional() private optionalService: OptionalService) {}Injection Tokens
Section titled “Injection Tokens”Tokens used to identify dependencies in the DI system
1. Type Tokens
Section titled “1. Type Tokens”Use the class itself as the token (most common)
providers: [MyService]constructor(private myService: MyService) {} // Type token2. InjectionToken
Section titled “2. InjectionToken”Create specific tokens for non-class dependencies
// Create tokenexport const API_URL = new InjectionToken<string>('API_URL');
// Provide valueproviders: [{ provide: API_URL, useValue: 'https://api.com' }]
// Inject using @Inject decoratorconstructor(@Inject(API_URL) private apiUrl: string) {}3. String Tokens (Legacy)
Section titled “3. String Tokens (Legacy)”String-based tokens (avoid in new code)
// Not recommended - potential naming conflictsproviders: [{ provide: 'ApiUrl', useValue: 'https://api.com' }]constructor(@Inject('ApiUrl') private apiUrl: string) {}Injection Scopes
Section titled “Injection Scopes”Different lifetimes and visibility scopes for dependencies
1. Singleton (Root Level)
Section titled “1. Singleton (Root Level)”One instance shared across entire application
@Injectable({ providedIn: "root", // Singleton service})export class MyService {}2. Module Level
Section titled “2. Module Level”One instance per module (deprecated in favor of ‘root’)
@NgModule({ providers: [MyService], // Module-level singleton})export class MyModule {}3. Component Level
Section titled “3. Component Level”New instance for each component instance
@Component({ providers: [MyService], // Instance per component})export class MyComponent {}4. Lazy-Loaded Module Scope
Section titled “4. Lazy-Loaded Module Scope”Separate instance for lazy-loaded modules
// In lazy module@NgModule({ providers: [MyService], // Separate instance for lazy module})export class LazyModule {}Hierarchical Injectors
Section titled “Hierarchical Injectors”How Angular’s injector hierarchy works and affects dependency resolution
// Injector Hierarchy:// Platform Injector (highest)// Root Injector// Module Injectors// Component Injectors (lowest)Parent-Child Resolution
Section titled “Parent-Child Resolution”Angular searches for dependencies up the injector tree
@Component({ selector: "parent", providers: [SharedService], // Available to parent and children})export class ParentComponent {}
@Component({ selector: "child",})export class ChildComponent { // Gets SharedService from parent's injector constructor(private sharedService: SharedService) {}}View Providers
Section titled “View Providers”Limits service visibility to current component view only
@Component({ viewProviders: [MyService], // Only available to this component, not content children})export class MyComponent {}Advanced DI Patterns
Section titled “Advanced DI Patterns”Complex dependency injection scenarios and solutions
1. Conditional Injection
Section titled “1. Conditional Injection”Inject different implementations based on conditions
providers: [ { provide: DataService, useFactory: () => { return environment.production ? new ProductionDataService() : new MockDataService(); }, },];2. Multi Providers
Section titled “2. Multi Providers”Provide multiple values for the same token
// Multiple plugins for the same tokenproviders: [ { provide: PLUGINS, useClass: LoggerPlugin, multi: true }, { provide: PLUGINS, useClass: AnalyticsPlugin, multi: true }]
// Inject all pluginsconstructor(@Inject(PLUGINS) private plugins: Plugin[]) {}3. Self and SkipSelf
Section titled “3. Self and SkipSelf”Control dependency lookup behavior
// @Self - only look in current injectorconstructor(@Self() private service: MyService) {}
// @SkipSelf - skip current injector, look in parentconstructor(@SkipSelf() private parentService: ParentService) {}4. Host Decorator
Section titled “4. Host Decorator”Limit lookup to current component and its host
// Only look in current component or its hostconstructor(@Host() private hostService: HostService) {}5. Custom Providers with useFactory
Section titled “5. Custom Providers with useFactory”Complex dependency creation with dependencies
providers: [ { provide: ComplexService, useFactory: (http: HttpClient, config: Config) => { return new ComplexService(http, config.apiUrl); }, deps: [HttpClient, Config], // Factory dependencies },];Best Practices
Section titled “Best Practices”Recommended patterns and guidelines for effective DI usage
1. Use providedIn: 'root' for Services
Section titled “1. Use providedIn: 'root' for Services”Prefer root-level providers for most services
@Injectable({ providedIn: "root", // ✅ Recommended})export class MyService {}2. Use InjectionToken for Non-Class Dependencies
Section titled “2. Use InjectionToken for Non-Class Dependencies”Always use InjectionToken for values, configs, and interfaces
export const APP_CONFIG = new InjectionToken<Config>("APP_CONFIG"); // ✅3. Avoid String Tokens
Section titled “3. Avoid String Tokens”Use InjectionToken instead of string tokens to avoid conflicts
// ❌ Avoid{ provide: 'apiUrl', useValue: '...' }
// ✅ Use insteadexport const API_URL = new InjectionToken('API_URL');4. Keep Constructors Simple
Section titled “4. Keep Constructors Simple”Use constructor only for dependency injection
// ✅ Goodconstructor( private serviceA: ServiceA, private serviceB: ServiceB) {}
// ❌ Avoid complex logic in constructorconstructor(private service: MyService) { this.service.initialize(); // Move to ngOnInit}5. Use Interface-Based DI with InjectionToken
Section titled “5. Use Interface-Based DI with InjectionToken”Inject implementations using interfaces
export interface Logger { log(message: string): void;}
export const LOGGER = new InjectionToken<Logger>('LOGGER');
providers: [{ provide: LOGGER, useClass: ConsoleLogger }]
constructor(@Inject(LOGGER) private logger: Logger) {}6. Proper Testing with DI
Section titled “6. Proper Testing with DI”Test components and services with mocked dependencies
// Test setupTestBed.configureTestingModule({ providers: [MyComponent, { provide: MyService, useClass: MockMyService }],});Common Injection Decorators Quick Reference
Section titled “Common Injection Decorators Quick Reference”| Decorator | Purpose | Usage Example |
|---|---|---|
@Injectable() | Marks class as injectable | @Injectable({providedIn: 'root'}) |
@Inject() | Inject using token | @Inject(API_URL) private url: string |
@Optional() | Make dependency optional | @Optional() private service: MyService |
@Self() | Only current injector | @Self() private service: MyService |
@SkipSelf() | Skip current injector | @SkipSelf() private parent: ParentService |
@Host() | Current or host injector | @Host() private hostService: HostService |