Skip to content

Angular DI

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
}

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

Fundamental building blocks of Angular’s Dependency Injection system

The container that holds dependencies and provides them when requested

// Angular creates injectors automatically
constructor(private injector: Injector) {}

Tells Angular how to create or deliver a dependency

// Providers tell Angular what to inject and how
providers: [MyService, { provide: API_URL, useValue: "https://api.com" }];

A class, value, or factory that another class needs to function

// Logger is a dependency of MyService
constructor(private logger: Logger) {}

Different ways to configure how dependencies are provided

Provides an instance of a class (most common)

providers: [MyService];
// Equivalent to:
providers: [{ provide: MyService, useClass: MyService }];

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" } },
];

Uses a factory function to create the dependency

providers: [
{
provide: MyService,
useFactory: (config: Config) => {
return config.production
? new ProductionService()
: new DevelopmentService();
},
deps: [Config],
},
];

Provides an existing token under a new token

providers: [
OldService,
{ provide: NewService, useExisting: OldService }, // Alias
];

Makes a dependency optional (won’t throw error if not found)

constructor(@Optional() private optionalService: OptionalService) {}

Tokens used to identify dependencies in the DI system

Use the class itself as the token (most common)

providers: [MyService]
constructor(private myService: MyService) {} // Type token

Create specific tokens for non-class dependencies

// Create token
export const API_URL = new InjectionToken<string>('API_URL');
// Provide value
providers: [{ provide: API_URL, useValue: 'https://api.com' }]
// Inject using @Inject decorator
constructor(@Inject(API_URL) private apiUrl: string) {}

String-based tokens (avoid in new code)

// Not recommended - potential naming conflicts
providers: [{ provide: 'ApiUrl', useValue: 'https://api.com' }]
constructor(@Inject('ApiUrl') private apiUrl: string) {}

Different lifetimes and visibility scopes for dependencies

One instance shared across entire application

@Injectable({
providedIn: "root", // Singleton service
})
export class MyService {}

One instance per module (deprecated in favor of ‘root’)

@NgModule({
providers: [MyService], // Module-level singleton
})
export class MyModule {}

New instance for each component instance

@Component({
providers: [MyService], // Instance per component
})
export class MyComponent {}

Separate instance for lazy-loaded modules

// In lazy module
@NgModule({
providers: [MyService], // Separate instance for lazy module
})
export class LazyModule {}

How Angular’s injector hierarchy works and affects dependency resolution

// Injector Hierarchy:
// Platform Injector (highest)
// Root Injector
// Module Injectors
// Component Injectors (lowest)

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) {}
}

Limits service visibility to current component view only

@Component({
viewProviders: [MyService], // Only available to this component, not content children
})
export class MyComponent {}

Complex dependency injection scenarios and solutions

Inject different implementations based on conditions

providers: [
{
provide: DataService,
useFactory: () => {
return environment.production
? new ProductionDataService()
: new MockDataService();
},
},
];

Provide multiple values for the same token

// Multiple plugins for the same token
providers: [
{ provide: PLUGINS, useClass: LoggerPlugin, multi: true },
{ provide: PLUGINS, useClass: AnalyticsPlugin, multi: true }
]
// Inject all plugins
constructor(@Inject(PLUGINS) private plugins: Plugin[]) {}

Control dependency lookup behavior

// @Self - only look in current injector
constructor(@Self() private service: MyService) {}
// @SkipSelf - skip current injector, look in parent
constructor(@SkipSelf() private parentService: ParentService) {}

Limit lookup to current component and its host

// Only look in current component or its host
constructor(@Host() private hostService: HostService) {}

Complex dependency creation with dependencies

providers: [
{
provide: ComplexService,
useFactory: (http: HttpClient, config: Config) => {
return new ComplexService(http, config.apiUrl);
},
deps: [HttpClient, Config], // Factory dependencies
},
];

Recommended patterns and guidelines for effective DI usage

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"); // ✅

Use InjectionToken instead of string tokens to avoid conflicts

// ❌ Avoid
{ provide: 'apiUrl', useValue: '...' }
// ✅ Use instead
export const API_URL = new InjectionToken('API_URL');

Use constructor only for dependency injection

// ✅ Good
constructor(
private serviceA: ServiceA,
private serviceB: ServiceB
) {}
// ❌ Avoid complex logic in constructor
constructor(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) {}

Test components and services with mocked dependencies

// Test setup
TestBed.configureTestingModule({
providers: [MyComponent, { provide: MyService, useClass: MockMyService }],
});

Common Injection Decorators Quick Reference

Section titled “Common Injection Decorators Quick Reference”
DecoratorPurposeUsage 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