Docs
/
Angular
Chapter 4
04 — Dependency Injection (DI)
What is DI?
Dependency Injection is a design pattern where a class receives its dependencies from an external source instead of creating them itself. Angular has a built-in DI framework that is central to the entire platform.
// ❌ Without DI — tightly coupled
class UserComponent {
private service = new UserService(new HttpClient(...)); // Manual creation
}
// ✅ With DI — Angular injects it
class UserComponent {
constructor(private service: UserService) {} // Angular resolves this
}
Providing Services
providedIn: 'root' — Singleton (Most Common)
@Injectable({ providedIn: 'root' })
export class AuthService {
// Single instance shared across the entire app
// Tree-shakable — removed from bundle if unused
}
Component-Level Provider — New Instance Per Component
@Component({
providers: [LoggerService], // New instance for this component + its children
})
export class DashboardComponent {
constructor(private logger: LoggerService) {}
}
Route-Level Provider
// app.routes.ts
export const routes: Routes = [
{
path: 'admin',
loadComponent: () => import('./admin.component').then(m => m.AdminComponent),
providers: [AdminService], // Scoped to this route and its children
},
];
app.config.ts — Application-Wide Providers
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(),
{ provide: API_URL, useValue: 'https://api.example.com' },
],
};
Provider Types
| Provider | Syntax | Use Case |
|---|---|---|
| Class | { provide: X, useClass: Y } | Replace implementation |
| Value | { provide: X, useValue: v } | Constants, configs |
| Factory | { provide: X, useFactory: fn } | Dynamic creation |
| Existing | { provide: X, useExisting: Y } | Alias one token to another |
useClass — Swap Implementations
// Interface (abstract class in Angular — interfaces aren't available at runtime)
abstract class StorageService {
abstract get(key: string): string | null;
abstract set(key: string, value: string): void;
}
// Implementations
@Injectable()
class LocalStorageService extends StorageService {
get(key: string) { return localStorage.getItem(key); }
set(key: string, value: string) { localStorage.setItem(key, value); }
}
@Injectable()
class SessionStorageService extends StorageService {
get(key: string) { return sessionStorage.getItem(key); }
set(key: string, value: string) { sessionStorage.setItem(key, value); }
}
// Provide
providers: [
{ provide: StorageService, useClass: LocalStorageService },
// Swap to SessionStorageService without changing any consumer code
]
useValue — Constants
providers: [
{ provide: API_URL, useValue: 'https://api.example.com' },
{ provide: APP_CONFIG, useValue: { debug: false, version: '2.0.0' } },
]
useFactory — Dynamic Creation
providers: [
{
provide: LoggerService,
useFactory: (config: AppConfig) => {
return config.debug
? new ConsoleLoggerService()
: new RemoteLoggerService();
},
deps: [APP_CONFIG], // Inject dependencies into factory
},
]
useExisting — Alias
providers: [
AuthService,
{ provide: AbstractAuth, useExisting: AuthService },
// Both tokens resolve to the SAME instance
]
Injection Tokens
Use InjectionToken when the dependency isn't a class (strings, objects, interfaces).
import { InjectionToken } from '@angular/core';
// Define token
export const API_URL = new InjectionToken<string>('API_URL');
export const APP_CONFIG = new InjectionToken<AppConfig>('APP_CONFIG');
export const IS_PRODUCTION = new InjectionToken<boolean>('IS_PRODUCTION');
// Provide
providers: [
{ provide: API_URL, useValue: environment.apiUrl },
{ provide: IS_PRODUCTION, useValue: environment.production },
]
// Inject
@Injectable({ providedIn: 'root' })
export class ApiService {
constructor(@Inject(API_URL) private apiUrl: string) {}
}
// Or with inject() function (Angular 14+)
export class ApiService {
private apiUrl = inject(API_URL);
}
inject() Function (Modern — Angular 14+)
Preferred over constructor injection. Works in components, services, directives, pipes, guards, and interceptors.
import { inject } from '@angular/core';
@Component({ ... })
export class DashboardComponent {
// Inject without constructor
private authService = inject(AuthService);
private router = inject(Router);
private apiUrl = inject(API_URL);
private destroyRef = inject(DestroyRef);
ngOnInit() {
this.authService.user$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(user => this.user = user);
}
}
inject() vs Constructor Injection
inject() | Constructor | |
|---|---|---|
| Syntax | Field initializer | Constructor parameter |
| Works in functions | ✅ (guards, interceptors) | ❌ |
| Less boilerplate | ✅ | ❌ |
| Testing | Same | Same |
| Recommendation | ✅ Preferred | Still works |
Hierarchical Injector Tree
Angular has a tree of injectors that mirrors the component tree.
Platform Injector
└── Root Injector (providedIn: 'root' / app.config providers)
├── Route Injector (route-level providers)
│ └── Component Injector (component-level providers)
│ └── Child Component Injector
└── Another Route Injector
Resolution order: Angular looks for the provider in the current injector, then walks up the tree until it finds one or throws an error.
Injection Decorators
constructor(
private auth: AuthService, // Normal — required
@Optional() private analytics?: AnalyticsService, // Don't throw if missing
@Self() private logger: LoggerService, // Only THIS injector
@SkipSelf() private parent: ParentService, // Skip THIS, look in parent
@Host() private host: HostService, // Up to host component only
) {}
| Decorator | Behavior |
|---|---|
| (none) | Walk up the entire tree |
@Optional() | Return null if not found (don't throw) |
@Self() | Only check current injector |
@SkipSelf() | Skip current, start from parent |
@Host() | Check up to the host component only |
Multi Providers
Multiple values for the same token — useful for plugin systems.
export const HTTP_INTERCEPTORS_TOKEN = new InjectionToken<HttpInterceptor[]>('interceptors');
providers: [
{ provide: HTTP_INTERCEPTORS_TOKEN, useClass: AuthInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS_TOKEN, useClass: LoggingInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS_TOKEN, useClass: ErrorInterceptor, multi: true },
]
// Injected as an array
@Injectable()
class HttpService {
interceptors = inject(HTTP_INTERCEPTORS_TOKEN); // HttpInterceptor[]
}
DestroyRef — Modern Cleanup (Angular 16+)
import { DestroyRef, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({ ... })
export class MyComponent {
private destroyRef = inject(DestroyRef);
ngOnInit() {
// Auto-unsubscribe when component is destroyed
someObservable$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(data => this.data = data);
// Manual cleanup callback
this.destroyRef.onDestroy(() => {
console.log('Component destroyed');
});
}
}
Key Takeaways
- Use
providedIn: 'root'for singleton services — it's tree-shakable - Use
inject()function over constructor injection — cleaner, works in functional guards/interceptors - Use
InjectionTokenfor non-class values (strings, configs, interfaces) - Use
@Optional()when a dependency might not exist - Component-level
providerscreate a new instance per component — useful for stateful services DestroyRef+takeUntilDestroyed()replaces the manualdestroy$subject pattern