Angular
02 / 10

Dependency Injection & Services

Angular Dependency Injection & Services

Dependency Injection (DI) is a core concept in Angular. It allows you to inject dependencies into components and services rather than creating them manually. Services are singleton objects that encapsulate business logic and data.

Creating Services

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, BehaviorSubject } from 'rxjs';
import { map, catchError } from 'rxjs/operators';

// Service with providedIn: 'root' (singleton)
@Injectable({
  providedIn: 'root' // Available app-wide
})
export class UserService {
  private apiUrl = 'https://api.example.com/users';
  private usersSubject = new BehaviorSubject<User[]>([]);
  public users$ = this.usersSubject.asObservable();
  
  constructor(private http: HttpClient) {
    this.loadUsers();
  }
  
  private loadUsers() {
    this.http.get<User[]>(this.apiUrl).subscribe(
      users => this.usersSubject.next(users)
    );
  }
  
  getUsers(): Observable<User[]> {
    return this.http.get<User[]>(this.apiUrl);
  }
  
  getUser(id: number): Observable<User> {
    return this.http.get<User>(`${this.apiUrl}/${id}`);
  }
  
  createUser(user: User): Observable<User> {
    return this.http.post<User>(this.apiUrl, user);
  }
  
  updateUser(id: number, user: User): Observable<User> {
    return this.http.put<User>(`${this.apiUrl}/${id}`, user);
  }
  
  deleteUser(id: number): Observable<void> {
    return this.http.delete<void>(`${this.apiUrl}/${id}`);
  }
}

interface User {
  id: number;
  name: string;
  email: string;
}

Injecting Services

import { Component, OnInit } from '@angular/core';
import { UserService } from './user.service';

@Component({
  selector: 'app-user-list',
  template: `
    <div>
      <h2>Users</h2>
      <ul>
        <li *ngFor="let user of users">
          {{ user.name }} - {{ user.email }}
        </li>
      </ul>
    </div>
  `
})
export class UserListComponent implements OnInit {
  users: User[] = [];
  
  // Constructor injection (traditional)
  constructor(private userService: UserService) { }
  
  ngOnInit() {
    this.userService.getUsers().subscribe(
      users => this.users = users
    );
  }
}

// Using inject() function (Angular 14+)
import { inject } from '@angular/core';

@Component({
  selector: 'app-user-detail',
  template: `<div>{{ user?.name }}</div>`
})
export class UserDetailComponent implements OnInit {
  // Modern inject() function
  private userService = inject(UserService);
  private route = inject(ActivatedRoute);
  
  user: User | null = null;
  
  ngOnInit() {
    const id = Number(this.route.snapshot.paramMap.get('id'));
    this.userService.getUser(id).subscribe(
      user => this.user = user
    );
  }
}

Provider Scope

// 1. Root level - singleton across entire app
@Injectable({
  providedIn: 'root'
})
export class GlobalService { }

// 2. Module level - singleton within module
@NgModule({
  providers: [ModuleScopedService]
})
export class FeatureModule { }

// 3. Component level - new instance per component
@Component({
  selector: 'app-example',
  providers: [ComponentScopedService] // New instance
})
export class ExampleComponent { }

// 4. Lazy loaded module - singleton within lazy module
@Injectable({
  providedIn: 'any' // New instance per lazy module
})
export class LazyService { }

Injection Tokens

InjectionTokens allow you to inject values that are not classes, like configuration objects.

import { InjectionToken } from '@angular/core';

// Define configuration interface
export interface AppConfig {
  apiUrl: string;
  production: boolean;
  version: string;
}

// Create injection token
export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');

// Provide value in module
@NgModule({
  providers: [
    {
      provide: APP_CONFIG,
      useValue: {
        apiUrl: 'https://api.example.com',
        production: false,
        version: '1.0.0'
      }
    }
  ]
})
export class AppModule { }

// Inject in component or service
@Injectable()
export class ApiService {
  constructor(@Inject(APP_CONFIG) private config: AppConfig) {
    console.log('API URL:', this.config.apiUrl);
  }
}

Hierarchical Injectors

// Parent component with service
@Component({
  selector: 'app-parent',
  providers: [SharedService], // Parent instance
  template: `
    <app-child-a></app-child-a>
    <app-child-b></app-child-b>
  `
})
export class ParentComponent {
  constructor(private shared: SharedService) {
    this.shared.setValue('from parent');
  }
}

// Child A - inherits parent's service instance
@Component({
  selector: 'app-child-a',
  template: `<p>{{ shared.getValue() }}</p>`
})
export class ChildAComponent {
  constructor(public shared: SharedService) { }
}

// Child B - has own instance
@Component({
  selector: 'app-child-b',
  providers: [SharedService], // Own instance
  template: `<p>{{ shared.getValue() }}</p>`
})
export class ChildBComponent {
  constructor(public shared: SharedService) {
    this.shared.setValue('child B');
  }
}

Optional and Self Decorators

import { Component, Optional, Self, SkipSelf, Host } from '@angular/core';

@Component({
  selector: 'app-example'
})
export class ExampleComponent {
  constructor(
    // Optional: Don't throw error if not found
    @Optional() private optionalService: OptionalService | null,
    
    // Self: Only look in current component
    @Self() private selfService: SelfService,
    
    // SkipSelf: Skip current component, look up hierarchy
    @SkipSelf() private parentService: ParentService,
    
    // Host: Look up to host component only
    @Host() private hostService: HostService
  ) {
    if (optionalService) {
      optionalService.doSomething();
    }
  }
}

Service Communication Pattern

import { Injectable } from '@angular/core';
import { BehaviorSubject, Subject, Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class StateService {
  // BehaviorSubject - has initial value, replays last value
  private dataSubject = new BehaviorSubject<any[]>([]);
  public data$ = this.dataSubject.asObservable();
  
  // Subject - no initial value, no replay
  private eventSubject = new Subject<string>();
  public events$ = this.eventSubject.asObservable();
  
  // Update data
  updateData(data: any[]) {
    this.dataSubject.next(data);
  }
  
  // Get current value
  getCurrentData(): any[] {
    return this.dataSubject.value;
  }
  
  // Emit event
  emitEvent(event: string) {
    this.eventSubject.next(event);
  }
}

// Component A - produces data
@Component({ /* ... */ })
export class ProducerComponent {
  constructor(private state: StateService) { }
  
  updateState() {
    this.state.updateData([1, 2, 3]);
    this.state.emitEvent('data-updated');
  }
}

// Component B - consumes data
@Component({ /* ... */ })
export class ConsumerComponent implements OnInit {
  data: any[] = [];
  
  constructor(private state: StateService) { }
  
  ngOnInit() {
    this.state.data$.subscribe(data => {
      this.data = data;
    });
    
    this.state.events$.subscribe(event => {
      console.log('Event:', event);
    });
  }
}

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free