Dependency Injection in Angular¶
What is Dependency Injection?¶
Dependency Injection (DI) is a design pattern and core feature of Angular that allows you to supply dependencies to a class rather than creating them inside the class. Angular's DI system makes your code more modular, testable, and maintainable.
Core Concepts¶
Dependencies and Dependents¶
// UserService is a dependency
@Injectable()
export class UserService {
getUsers() {
return ['John', 'Jane', 'Bob'];
}
}
// UserComponent is the dependent
@Component({
selector: 'app-user',
template: '<ul><li *ngFor="let user of users">{{ user }}</li></ul>'
})
export class UserComponent {
users: string[];
// UserService is injected into the constructor
constructor(private userService: UserService) {
this.users = this.userService.getUsers();
}
}
Injection Tokens¶
Angular uses injection tokens to identify dependencies:
import { InjectionToken } from '@angular/core';
// Creating an injection token
export const API_URL = new InjectionToken<string>('api.url');
export const CONFIG = new InjectionToken<AppConfig>('app.config');
// Using injection tokens
@Component({
selector: 'app-config-display'
})
export class ConfigDisplayComponent {
constructor(
@Inject(API_URL) private apiUrl: string,
@Inject(CONFIG) private config: AppConfig
) {
console.log('API URL:', apiUrl);
console.log('Config:', config);
}
}
Creating Injectable Services¶
Basic Service¶
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root' // Makes it a singleton available app-wide
})
export class LoggingService {
private logs: string[] = [];
log(message: string) {
const timestamp = new Date().toISOString();
const logEntry = `[${timestamp}] ${message}`;
this.logs.push(logEntry);
console.log(logEntry);
}
getLogs(): string[] {
return [...this.logs];
}
clearLogs() {
this.logs = [];
}
}
Service with Dependencies¶
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface User {
id: number;
name: string;
email: string;
}
@Injectable({
providedIn: 'root'
})
export class UserService {
private apiUrl = 'https://jsonplaceholder.typicode.com/users';
constructor(
private http: HttpClient,
private loggingService: LoggingService
) {}
getUsers(): Observable<User[]> {
this.loggingService.log('Fetching users from API');
return this.http.get<User[]>(this.apiUrl);
}
getUser(id: number): Observable<User> {
this.loggingService.log(`Fetching user with id: ${id}`);
return this.http.get<User>(`${this.apiUrl}/${id}`);
}
createUser(user: Partial<User>): Observable<User> {
this.loggingService.log('Creating new user');
return this.http.post<User>(this.apiUrl, user);
}
}
Provider Configuration¶
Root Level Providers¶
Using providedIn¶
@Injectable({
providedIn: 'root' // Singleton across the entire app
})
export class GlobalService {
// Service implementation
}
Using providers in main.ts (Standalone)¶
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, {
providers: [
GlobalService,
{ provide: LoggingService, useClass: LoggingService },
{ provide: API_URL, useValue: 'https://api.example.com' }
]
});
Using providers in AppModule¶
@NgModule({
providers: [
GlobalService,
{ provide: API_URL, useValue: 'https://api.example.com' }
]
})
export class AppModule { }
Component Level Providers¶
@Component({
selector: 'app-user-profile',
providers: [
UserProfileService, // New instance for each component instance
{ provide: COMPONENT_ID, useValue: 'user-profile-123' }
],
template: `
<h2>User Profile</h2>
<p>Profile ID: {{ profileId }}</p>
`
})
export class UserProfileComponent {
profileId: string;
constructor(
private userProfileService: UserProfileService,
@Inject(COMPONENT_ID) componentId: string
) {
this.profileId = componentId;
}
}
Provider Types¶
Class Providers¶
// Basic class provider
{ provide: UserService, useClass: UserService }
// Alternative implementation
{ provide: UserService, useClass: MockUserService }
// Abstract class implementation
abstract class BaseApiService {
abstract getData(): Observable<any>;
}
@Injectable()
class ApiService extends BaseApiService {
getData(): Observable<any> {
// Implementation
}
}
// Provider configuration
{ provide: BaseApiService, useClass: ApiService }
Value Providers¶
const appConfig = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3
};
// Value provider
{ provide: 'APP_CONFIG', useValue: appConfig }
// Using in component
constructor(@Inject('APP_CONFIG') private config: any) {
console.log('API URL:', config.apiUrl);
}
Factory Providers¶
// Factory function
function createLogger(isDevelopment: boolean): LoggingService {
if (isDevelopment) {
return new ConsoleLoggingService();
} else {
return new RemoteLoggingService();
}
}
// Factory provider
{
provide: LoggingService,
useFactory: createLogger,
deps: [IS_DEVELOPMENT] // Dependencies for the factory function
}
// Complex factory example
function createHttpClient(apiUrl: string, authService: AuthService): HttpClient {
const httpClient = new HttpClient();
// Configure httpClient with interceptors, base URL, etc.
return httpClient;
}
{
provide: 'CONFIGURED_HTTP_CLIENT',
useFactory: createHttpClient,
deps: [API_URL, AuthService]
}
Existing Provider¶
// Use existing provider
{ provide: 'LOGGER', useExisting: LoggingService }
// Useful for aliasing
{ provide: 'USER_API', useExisting: UserService }
Injection Strategies¶
Constructor Injection¶
@Component({
selector: 'app-dashboard'
})
export class DashboardComponent {
constructor(
private userService: UserService,
private loggingService: LoggingService,
@Inject(API_URL) private apiUrl: string
) {}
}
Optional Dependencies¶
import { Optional } from '@angular/core';
@Component({
selector: 'app-feature'
})
export class FeatureComponent {
constructor(
private requiredService: RequiredService,
@Optional() private optionalService?: OptionalService
) {
if (this.optionalService) {
// Use optional service if available
this.optionalService.doSomething();
}
}
}
Self and SkipSelf¶
import { Self, SkipSelf } from '@angular/core';
@Component({
selector: 'app-parent',
providers: [SharedService]
})
export class ParentComponent {}
@Component({
selector: 'app-child'
})
export class ChildComponent {
constructor(
@Self() private selfService: SharedService, // Only from this component
@SkipSelf() private parentService: SharedService // Skip this component, look in parent
) {}
}
Host Decorator¶
import { Host } from '@angular/core';
@Component({
selector: 'app-child'
})
export class ChildComponent {
constructor(
@Host() private hostService: HostService // Only from host component
) {}
}
Multi Providers¶
Collecting Multiple Providers¶
// Define multi-provider token
export const PLUGIN_TOKEN = new InjectionToken<Plugin[]>('plugins');
// Register multiple providers
export const PLUGIN_PROVIDERS = [
{ provide: PLUGIN_TOKEN, useClass: PluginA, multi: true },
{ provide: PLUGIN_TOKEN, useClass: PluginB, multi: true },
{ provide: PLUGIN_TOKEN, useClass: PluginC, multi: true }
];
// Use in component
@Component({
providers: [PLUGIN_PROVIDERS]
})
export class PluginManagerComponent {
constructor(@Inject(PLUGIN_TOKEN) private plugins: Plugin[]) {
// All plugins are available as an array
plugins.forEach(plugin => plugin.initialize());
}
}
HTTP Interceptors Example¶
import { HTTP_INTERCEPTORS } from '@angular/common/http';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// Add auth header
const authReq = req.clone({
headers: req.headers.set('Authorization', 'Bearer token')
});
return next.handle(authReq);
}
}
// Provider configuration
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true // Allows multiple interceptors
}
Hierarchical Injection¶
Understanding the Injector Tree¶
AppModule Injector (Root)
├── FeatureModule Injector
│ ├── FeatureComponent Injector
│ └── AnotherComponent Injector
└── SharedModule Injector
└── SharedComponent Injector
Resolution Strategy¶
Angular searches for providers in this order:
- Current component's injector
- Parent component's injector
- Module injector
- Root injector
Example of Hierarchical DI¶
// Root level service
@Injectable({ providedIn: 'root' })
export class GlobalCounterService {
count = 0;
increment() { this.count++; }
}
// Module level service
@Injectable()
export class ModuleCounterService {
count = 0;
increment() { this.count++; }
}
// Feature module
@NgModule({
providers: [ModuleCounterService]
})
export class FeatureModule {}
// Component with its own provider
@Component({
selector: 'app-counter',
providers: [ModuleCounterService], // Component-specific instance
template: `
<div>
<p>Global Count: {{ globalCounter.count }}</p>
<p>Module Count: {{ moduleCounter.count }}</p>
<button (click)="increment()">Increment</button>
</div>
`
})
export class CounterComponent {
constructor(
public globalCounter: GlobalCounterService, // Shared instance
public moduleCounter: ModuleCounterService // Component-specific instance
) {}
increment() {
this.globalCounter.increment();
this.moduleCounter.increment();
}
}
Service Scoping¶
Singleton Services¶
// Root singleton - shared across entire app
@Injectable({ providedIn: 'root' })
export class SingletonService {
private data: any[] = [];
addData(item: any) {
this.data.push(item);
}
getData() {
return this.data;
}
}
Module-Scoped Services¶
// Service scoped to a module
@Injectable()
export class FeatureScopedService {
// New instance for each module that imports it
}
@NgModule({
providers: [FeatureScopedService]
})
export class FeatureModule {}
Component-Scoped Services¶
// Service scoped to component and its children
@Injectable()
export class ComponentScopedService {
// New instance for each component that provides it
}
@Component({
providers: [ComponentScopedService]
})
export class MyComponent {}
Advanced DI Patterns¶
Service Locator Pattern¶
@Injectable({ providedIn: 'root' })
export class ServiceLocator {
constructor(private injector: Injector) {}
getService<T>(token: any): T {
return this.injector.get(token);
}
}
// Usage
@Component({})
export class DynamicComponent {
constructor(private serviceLocator: ServiceLocator) {
const userService = this.serviceLocator.getService(UserService);
}
}
Dynamic Service Creation¶
import { Injector, ComponentFactoryResolver } from '@angular/core';
@Injectable()
export class DynamicServiceFactory {
constructor(private injector: Injector) {}
createService<T>(serviceClass: new(...args: any[]) => T): T {
const service = this.injector.get(serviceClass);
return service;
}
}
Conditional Providers¶
import { environment } from '../environments/environment';
// Conditional provider based on environment
export const LOGGER_PROVIDER = {
provide: LoggingService,
useClass: environment.production ? ProductionLogger : DevelopmentLogger
};
// Feature flag provider
export const FEATURE_PROVIDERS = environment.enableBetaFeatures
? [BetaFeatureService, ExperimentalService]
: [StandardFeatureService];
Testing with Dependency Injection¶
Mocking Dependencies¶
import { TestBed } from '@angular/core/testing';
describe('UserComponent', () => {
let component: UserComponent;
let mockUserService: jasmine.SpyObj<UserService>;
beforeEach(() => {
const spy = jasmine.createSpyObj('UserService', ['getUsers', 'getUser']);
TestBed.configureTestingModule({
declarations: [UserComponent],
providers: [
{ provide: UserService, useValue: spy }
]
});
mockUserService = TestBed.inject(UserService) as jasmine.SpyObj<UserService>;
});
it('should call getUserService', () => {
mockUserService.getUsers.and.returnValue(of(['test user']));
component.loadUsers();
expect(mockUserService.getUsers).toHaveBeenCalled();
});
});
Testing Providers¶
describe('UserService', () => {
let service: UserService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [UserService]
});
service = TestBed.inject(UserService);
httpMock = TestBed.inject(HttpTestingController);
});
it('should fetch users', () => {
const mockUsers = [{ id: 1, name: 'John' }];
service.getUsers().subscribe(users => {
expect(users).toEqual(mockUsers);
});
const req = httpMock.expectOne('/api/users');
expect(req.request.method).toBe('GET');
req.flush(mockUsers);
});
});
Best Practices¶
1. Use Interface Segregation¶
// Define focused interfaces
interface UserReader {
getUser(id: number): Observable<User>;
getUsers(): Observable<User[]>;
}
interface UserWriter {
createUser(user: User): Observable<User>;
updateUser(user: User): Observable<User>;
deleteUser(id: number): Observable<void>;
}
// Implement specific interfaces
@Injectable()
export class UserService implements UserReader, UserWriter {
// Implementation
}
// Inject specific interfaces where needed
@Component({})
export class UserListComponent {
constructor(private userReader: UserReader) {} // Only needs read operations
}
2. Avoid Circular Dependencies¶
// Bad - circular dependency
@Injectable()
export class ServiceA {
constructor(private serviceB: ServiceB) {}
}
@Injectable()
export class ServiceB {
constructor(private serviceA: ServiceA) {}
}
// Good - use shared interface or event system
export interface EventBus {
emit(event: string, data: any): void;
on(event: string, callback: Function): void;
}
@Injectable()
export class ServiceA {
constructor(private eventBus: EventBus) {}
}
@Injectable()
export class ServiceB {
constructor(private eventBus: EventBus) {}
}
3. Use Abstract Classes for Service Contracts¶
export abstract class ApiService {
abstract get<T>(url: string): Observable<T>;
abstract post<T>(url: string, data: any): Observable<T>;
}
@Injectable()
export class HttpApiService extends ApiService {
constructor(private http: HttpClient) {
super();
}
get<T>(url: string): Observable<T> {
return this.http.get<T>(url);
}
post<T>(url: string, data: any): Observable<T> {
return this.http.post<T>(url, data);
}
}
// Provider
{ provide: ApiService, useClass: HttpApiService }
4. Lazy Service Loading¶
@Injectable({
providedIn: 'root'
})
export class LazyServiceLoader {
private services = new Map<string, Promise<any>>();
async loadService<T>(modulePath: string, serviceName: string): Promise<T> {
if (!this.services.has(serviceName)) {
const modulePromise = import(modulePath).then(module => {
return module[serviceName];
});
this.services.set(serviceName, modulePromise);
}
return this.services.get(serviceName)!;
}
}
Common Pitfalls¶
1. Overusing Injection¶
// Bad - too many dependencies
@Component({})
export class OverloadedComponent {
constructor(
private service1: Service1,
private service2: Service2,
private service3: Service3,
private service4: Service4,
private service5: Service5
) {}
}
// Good - use facade pattern
@Injectable()
export class ComponentFacade {
constructor(
private service1: Service1,
private service2: Service2,
private service3: Service3
) {}
}
@Component({})
export class CleanComponent {
constructor(private facade: ComponentFacade) {}
}
2. Memory Leaks with Singleton Services¶
// Bad - holds references
@Injectable({ providedIn: 'root' })
export class LeakyService {
private components: any[] = [];
registerComponent(component: any) {
this.components.push(component); // Memory leak!
}
}
// Good - use WeakMap or cleanup
@Injectable({ providedIn: 'root' })
export class CleanService {
private components = new WeakMap();
registerComponent(component: any, data: any) {
this.components.set(component, data);
}
}
Summary¶
Angular's Dependency Injection system provides:
- Inversion of Control: Dependencies are provided rather than created
- Testability: Easy to mock dependencies for testing
- Modularity: Services can be swapped without changing consumers
- Hierarchy: Different scopes for different use cases
- Flexibility: Multiple provider types and configuration options
Key concepts:
- Services are classes with
@Injectable()decorator - Providers configure how dependencies are created
- Injection hierarchy determines service scope
- Multiple provider types support different scenarios
- Proper testing requires dependency mocking
Next Steps¶
Now that you understand Dependency Injection, you're ready to explore:
- Creating and using Angular services
- HTTP client and API communication
- State management patterns
- Advanced service patterns
- Testing strategies
Dependency Injection is the foundation of Angular's architecture - master it to build maintainable and testable applications!