State Management & RxJS

State Management in Angular

Learn state management patterns in Angular โ€” service-based state, BehaviorSubject stores, Angular Signals, and when to use dedicated state management libraries.

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

TypeExampleScope
LocalForm inputs, toggle flagsSingle component
SharedShopping cart, user authMultiple components
ServerAPI data, cached responsesEntire app
URLRoute params, query stringsURL-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

FeatureSignalsBehaviorSubject
Syntaxcount()count$ | async
SubscriptionAutomatic (template)Manual or async pipe
Computed valuescomputed()pipe(map())
Side effectseffect()subscribe()
Memory managementAutomaticMust unsubscribe
Interop with RxJStoObservable() / toSignal()Native
Best forComponent stateComplex 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

ApproachWhen to Use
Component stateLocal UI state (toggles, form data)
SignalsShared state with simple derivations
BehaviorSubject servicesShared state with complex async flows
NgRx / NGXSVery 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 service

Next 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.