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?
2. Why should you create new references when updating state?
3. What is the facade pattern in Angular services?
Flashcards
Question
How should services expose state?
Click to reveal answer
Answer
As Observable streams (items$) or signals (asReadonly()). Components subscribe and react to changes.
Question
Why create new references when updating state?
Click to reveal answer
Answer
Change detection uses reference equality. New references trigger updates; mutations do not.
Question
What is the facade pattern?
Click to reveal answer
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)
);
}