What is State Management?
State is the data that drives your application's UI โ user information, form values, API responses, UI flags, and more. State management is about organizing, sharing, and updating this data predictably.
Types of State
| Type | Example | Scope |
|---|---|---|
| Local | Form inputs, toggle flags | Single component |
| Shared | Shopping cart, user auth | Multiple components |
| Server | API data, cached responses | Entire app |
| URL | Route params, query strings | URL-driven |
Service-Based State (The Angular Way)
The most idiomatic Angular approach uses services with BehaviorSubject:
Simple Store Service
typescript
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
export interface AppState {
user: User | null;
isLoading: boolean;
notifications: Notification[];
}
const initialState: AppState = {
user: null,
isLoading: false,
notifications: [],
};
@Injectable({ providedIn: 'root' })
export class AppStateService {
private state = new BehaviorSubject<AppState>(initialState);
// Expose as readonly observable
state$: Observable<AppState> = this.state.asObservable();
// Selectors
get user$() { return this.select(s => s.user); }
get isLoading$() { return this.select(s => s.isLoading); }
get notifications$() { return this.select(s => s.notifications); }
// Update methods
setUser(user: User | null) {
this.updateState({ user });
}
setLoading(isLoading: boolean) {
this.updateState({ isLoading });
}
addNotification(notification: Notification) {
const current = this.state.value.notifications;
this.updateState({ notifications: [...current, notification] });
}
// Helpers
private select<T>(selector: (state: AppState) => T): Observable<T> {
return this.state$.pipe(
map(selector),
distinctUntilChanged(),
);
}
private updateState(partial: Partial<AppState>) {
this.state.next({ ...this.state.value, ...partial });
}
}Shopping Cart Store
typescript
interface CartState {
items: CartItem[];
isOpen: boolean;
}
@Injectable({ providedIn: 'root' })
export class CartStore {
private state = new BehaviorSubject<CartState>({
items: [],
isOpen: false,
});
// Selectors
items$ = this.state.pipe(map(s => s.items), distinctUntilChanged());
isOpen$ = this.state.pipe(map(s => s.isOpen), distinctUntilChanged());
itemCount$ = this.items$.pipe(map(items => items.reduce((sum, i) => sum + i.qty, 0)));
total$ = this.items$.pipe(map(items => items.reduce((sum, i) => sum + i.price * i.qty, 0)));
addItem(product: Product) {
const items = [...this.state.value.items];
const existing = items.find(i => i.productId === product.id);
if (existing) {
existing.qty++;
} else {
items.push({ productId: product.id, name: product.name, price: product.price, qty: 1 });
}
this.state.next({ ...this.state.value, items });
}
removeItem(productId: number) {
const items = this.state.value.items.filter(i => i.productId !== productId);
this.state.next({ ...this.state.value, items });
}
toggleCart() {
this.state.next({ ...this.state.value, isOpen: !this.state.value.isOpen });
}
clear() {
this.state.next({ ...this.state.value, items: [] });
}
}Usage in Components
typescript
@Component({
template: `
<div class="cart-icon" (click)="cartStore.toggleCart()">
๐ {{ cartStore.itemCount$ | async }}
</div>
@if (cartStore.isOpen$ | async) {
<div class="cart-panel">
@for (item of cartStore.items$ | async; track item.productId) {
<div>{{ item.name }} ร {{ item.qty }} = {{ item.price * item.qty | currency }}</div>
}
<p>Total: {{ cartStore.total$ | async | currency }}</p>
</div>
}
`,
})
export class CartWidgetComponent {
cartStore = inject(CartStore);
}Angular Signals (Angular 16+)
Signals are a new reactive primitive in Angular that simplify state management without RxJS:
Basic Signals
typescript
import { signal, computed, effect } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<h2>Count: {{ count() }}</h2>
<p>Double: {{ double() }}</p>
<button (click)="increment()">+</button>
<button (click)="decrement()">-</button>
<button (click)="reset()">Reset</button>
`,
})
export class CounterComponent {
// Writable signal
count = signal(0);
// Computed signal (derived state)
double = computed(() => this.count() * 2);
increment() { this.count.update(c => c + 1); }
decrement() { this.count.update(c => c - 1); }
reset() { this.count.set(0); }
}Signal API
typescript
// Create a signal
const name = signal('Angular');
// Read the value
console.log(name()); // 'Angular'
// Set a new value
name.set('React');
// Update based on current value
name.update(current => current.toUpperCase());
// Computed (derived) signal
const greeting = computed(() => `Hello, ${name()}!`);
// Effect (side effect when signals change)
effect(() => {
console.log(`Name changed to: ${name()}`);
// Runs whenever name() changes
});Signal-Based Service
typescript
@Injectable({ providedIn: 'root' })
export class TodoStore {
// State as signals
private _todos = signal<Todo[]>([]);
private _filter = signal<'all' | 'active' | 'completed'>('all');
// Public read-only signals
todos = this._todos.asReadonly();
filter = this._filter.asReadonly();
// Derived state
filteredTodos = computed(() => {
const todos = this._todos();
const filter = this._filter();
switch (filter) {
case 'active': return todos.filter(t => !t.completed);
case 'completed': return todos.filter(t => t.completed);
default: return todos;
}
});
activeCount = computed(() =>
this._todos().filter(t => !t.completed).length
);
// Actions
addTodo(title: string) {
this._todos.update(todos => [
...todos,
{ id: Date.now(), title, completed: false },
]);
}
toggleTodo(id: number) {
this._todos.update(todos =>
todos.map(t => t.id === id ? { ...t, completed: !t.completed } : t)
);
}
removeTodo(id: number) {
this._todos.update(todos => todos.filter(t => t.id !== id));
}
setFilter(filter: 'all' | 'active' | 'completed') {
this._filter.set(filter);
}
}Using Signal Store in Components
typescript
@Component({
selector: 'app-todo-list',
standalone: true,
template: `
<div>
<input #input placeholder="New todo">
<button (click)="addTodo(input)">Add</button>
</div>
<nav>
<button (click)="store.setFilter('all')" [class.active]="store.filter() === 'all'">All</button>
<button (click)="store.setFilter('active')" [class.active]="store.filter() === 'active'">Active</button>
<button (click)="store.setFilter('completed')" [class.active]="store.filter() === 'completed'">Completed</button>
</nav>
<p>{{ store.activeCount() }} items remaining</p>
@for (todo of store.filteredTodos(); track todo.id) {
<div [class.completed]="todo.completed">
<input type="checkbox" [checked]="todo.completed" (change)="store.toggleTodo(todo.id)">
{{ todo.title }}
<button (click)="store.removeTodo(todo.id)">โ</button>
</div>
}
`,
})
export class TodoListComponent {
store = inject(TodoStore);
addTodo(input: HTMLInputElement) {
if (input.value.trim()) {
this.store.addTodo(input.value.trim());
input.value = '';
}
}
}Signals vs BehaviorSubject
| Feature | Signals | BehaviorSubject |
|---|---|---|
| Syntax | count() | count$ | async |
| Subscription | Automatic (template) | Manual or async pipe |
| Computed values | computed() | pipe(map()) |
| Side effects | effect() | subscribe() |
| Memory management | Automatic | Must unsubscribe |
| Interop with RxJS | toObservable() / toSignal() | Native |
| Best for | Component state | Complex async streams |
Signal โ Observable Interop
typescript
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
// Observable โ Signal
const users = toSignal(this.userService.getUsers(), { initialValue: [] });
// Signal โ Observable
const count = signal(0);
const count$ = toObservable(count);Choosing a State Management Approach
| Approach | When to Use |
|---|---|
| Component state | Local UI state (toggles, form data) |
| Signals | Shared state with simple derivations |
| BehaviorSubject services | Shared state with complex async flows |
| NgRx / NGXS | Very large apps needing strict patterns, time-travel debugging |
Decision Guide
Is state local to one component?
โโโถ Yes โ Use component properties / signals
โโโถ No โ Is it simple shared state?
โโโถ Yes โ Signal-based service
โโโถ No โ Does it involve complex async?
โโโถ Yes โ BehaviorSubject service
โโโถ No โ Is the team large with strict patterns needed?
โโโถ Yes โ NgRx / NGXS
โโโถ No โ BehaviorSubject or Signal serviceNext Steps
With state management strategies in hand, let's move to the final module โ advanced topics including Angular Material UI, testing, performance optimization, and production deployment.