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

ProviderSyntaxUse 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
SyntaxField initializerConstructor parameter
Works in functions✅ (guards, interceptors)
Less boilerplate
TestingSameSame
Recommendation✅ PreferredStill 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
) {}
DecoratorBehavior
(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 InjectionToken for non-class values (strings, configs, interfaces)
  • Use @Optional() when a dependency might not exist
  • Component-level providers create a new instance per component — useful for stateful services
  • DestroyRef + takeUntilDestroyed() replaces the manual destroy$ subject pattern