Skip to content
intermediate Phase 5 · Services and Dependency Injection

Service Patterns for State and Side Effects

Put shared state and side-effect logic into well-shaped services.

40m
0 problems
Topic Progress 0%

State Management in Services

Services are the primary place for shared state in Angular. The key principle is to expose state as Observables so components can react to changes.

BehaviorSubject Pattern

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

@Injectable({ providedIn: 'root' })
export class CartService {
  private items = new BehaviorSubject<CartItem[]>([]);
  items$ = this.items.asObservable();

  addItem(item: CartItem): void {
    const current = this.items.getValue();
    this.items.next([...current, item]);
  }

  removeItem(id: number): void {
    const current = this.items.getValue();
    this.items.next(current.filter(i => i.id !== id));
  }

  get total(): number {
    return this.items.getValue().reduce((sum, item) => sum + item.price, 0);
  }

  clear(): void {
    this.items.next([]);
  }
}

Signal-Based State (Modern)

import { Injectable, signal, computed } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class CartService {
  private items = signal<CartItem[]>([]);
  readonly cartItems = this.items.asReadonly();

  readonly total = computed(() =>
    this.items().reduce((sum, item) => sum + item.price, 0)
  );

  readonly count = computed(() => this.items().length);

  addItem(item: CartItem): void {
    this.items.update(items => [...items, item]);
  }

  removeItem(id: number): void {
    this.items.update(items => items.filter(i => i.id !== id));
  }
}

Why Expose Observables Instead of Raw Values?

// BAD: Components cannot react to changes
get items(): CartItem[] {
  return this.items;
}

// GOOD: Components subscribe and react
items$: Observable<CartItem[]>;

Immutable State Updates

Always create new references when updating state. This ensures change detection works correctly and OnPush components re-render.

// BAD: Mutating the array
this.items.push(newItem);
this.items$.next(this.items); // Same reference — no change detected

// GOOD: New array reference
this.items$.next([...this.items.getValue(), newItem]);

API Service Pattern

API services encapsulate HTTP communication. They return Observables and handle errors consistently.

Basic API Service

import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, catchError, throwError } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class UserService {
  private http = inject(HttpClient);
  private baseUrl = '/api/users';

  getUsers(): Observable<User[]> {
    return this.http.get<User[]>(this.baseUrl).pipe(
      catchError(this.handleError)
    );
  }

  getUser(id: number): Observable<User> {
    return this.http.get<User>(`${this.baseUrl}/${id}`).pipe(
      catchError(this.handleError)
    );
  }

  createUser(user: CreateUserRequest): Observable<User> {
    return this.http.post<User>(this.baseUrl, user).pipe(
      catchError(this.handleError)
    );
  }

  updateUser(id: number, user: Partial<User>): Observable<User> {
    return this.http.put<User>(`${this.baseUrl}/${id}`, user).pipe(
      catchError(this.handleError)
    );
  }

  deleteUser(id: number): Observable<void> {
    return this.http.delete<void>(`${this.baseUrl}/${id}`).pipe(
      catchError(this.handleError)
    );
  }

  private handleError(error: HttpErrorResponse): Observable<never> {
    console.error('API error:', error);
    return throwError(() => new Error(error.message));
  }
}

Loading State Pattern

@Injectable({ providedIn: 'root' })
export class UserService {
  private loading = new BehaviorSubject<boolean>(false);
  loading$ = this.loading.asObservable();

  getUsers(): Observable<User[]> {
    this.loading.next(true);
    return this.http.get<User[]>(this.baseUrl).pipe(
      finalize(() => this.loading.next(false))
    );
  }
}

Facade Pattern

A facade service combines multiple services into a single simplified API for components:

@Injectable({ providedIn: 'root' })
export class UserFacade {
  private userService = inject(UserService);
  private authService = inject(AuthService);

  currentUser$ = this.authService.currentUser$;
  users$ = this.userService.users$;

  loadUsers(): void {
    this.userService.loadUsers();
  }

  deleteUser(id: number): void {
    this.userService.deleteUser(id);
  }
}

Quiz

1. Why should services expose state as Observables instead of raw values?

Question 1 options

2. Why should you create new references when updating state?

Question 2 options

3. What is the facade pattern in Angular services?

Question 3 options

Flashcards

Question

How should services expose state?

Answer

As Observable streams (items$) or signals (asReadonly()). Components subscribe and react to changes.

Question

Why create new references when updating state?

Answer

Change detection uses reference equality. New references trigger updates; mutations do not.

Question

What is the facade pattern?

Answer

A service that combines multiple services into a single simplified API for components.

Revision Notes

Key Takeaways

  • 1. Expose state as Observables or signals for reactivity
  • 2. Always create new references when updating state
  • 3. API services encapsulate HTTP and error handling
  • 4. Facade pattern simplifies component-service interaction

Interview Tips

  • • Explain why Observables are preferred over raw values for state
  • • Be ready to describe immutable state updates
  • • Mention the facade pattern for complex service interactions

Cheat Sheet

Service Patterns Cheat Sheet

State service:

private items = new BehaviorSubject<CartItem[]>([]);
items$ = this.items.asObservable();

addItem(item: CartItem): void {
  this.items.next([...this.items.getValue(), item]);
}

Signal state:

private items = signal<CartItem[]>([]);
readonly cartItems = this.items.asReadonly();

API service:

getUsers(): Observable<User[]> {
  return this.http.get<User[]>(this.baseUrl).pipe(
    catchError(this.handleError)
  );
}

Continue Learning

Before You Start

Next Topics

Related Reading