AngularJSState Management Overview

State Management in Angular

As Angular applications grow, state management becomes critical. "State" is any data that determines what your UI looks like: the current user, a product list, a shopping cart, UI flags, and more.

Without a strategy, state scatters across components and services, making bugs hard to reproduce and UI hard to reason about. Angular offers several state management options — from lightweight built-in tools to fully-featured libraries.

Types of State

Type

Description

Example

Local / UI state

Belongs to a single component

dropdown open, form dirty, tab index

Shared state

Used by multiple components

current user, theme, language

Server state

Data fetched from an API

product list, user profile

Router state

Current URL and params

active route, query params

Form state

Form values and validation

Reactive form model

State Management Options in Angular

Approach

Complexity

Best For

Component @Input/@Output

Minimal

Simple parent-child data flow

Services + BehaviorSubject

Low

Small to medium apps, shared state

Services + Signals

Low

Modern Angular 16+, simple reactive state

NgRx Store

High

Large teams, complex state, time-travel debugging

NgRx ComponentStore

Medium

Complex per-component state

NgRx SignalStore

Medium

Modern signal-based NgRx (Angular 17+)

Akita / Elf

Medium

Alternative to NgRx with less boilerplate

Approach 1: Component State (Local)

The simplest approach — store state directly in the component. Good for UI-only state that nothing else needs.

TS
@Component({
  selector: 'app-dropdown',
  standalone: true,
  template: `
    <button (click)="toggle()">Menu</button>
    @if (isOpen) {
      <ul>
        <li>Option 1</li>
        <li>Option 2</li>
      </ul>
    }
  `,
})
export class DropdownComponent {
  isOpen = false;
  toggle(): void { this.isOpen = !this.isOpen; }
}
Approach 2: Service + BehaviorSubject

For state shared across multiple components, move it into a singleton service backed by a BehaviorSubject. This is the most common pattern for small to medium Angular apps.

TS
// src/app/services/auth.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';

export interface User {
  id: number;
  name: string;
  email: string;
  roles: string[];
}

@Injectable({ providedIn: 'root' })
export class AuthService {
  private currentUser$ = new BehaviorSubject<User | null>(null);

  readonly user$: Observable<User | null> = this.currentUser$.asObservable();
  readonly isLoggedIn$: Observable<boolean> = this.user$.pipe(map(u => !!u));
  readonly isAdmin$: Observable<boolean> = this.user$.pipe(
    map(u => u?.roles.includes('admin') ?? false)
  );

  constructor(private http: HttpClient) {
    // Rehydrate from storage on startup
    const stored = localStorage.getItem('user');
    if (stored) this.currentUser$.next(JSON.parse(stored));
  }

  login(email: string, password: string): Observable<User> {
    return this.http.post<User>('/api/auth/login', { email, password }).pipe(
      tap(user => {
        this.currentUser$.next(user);
        localStorage.setItem('user', JSON.stringify(user));
      })
    );
  }

  logout(): void {
    this.currentUser$.next(null);
    localStorage.removeItem('user');
  }

  getUser(): User | null {
    return this.currentUser$.getValue();
  }
}
Approach 3: Service + Signals (Angular 16+)

Angular Signals offer a simpler reactive primitive for state management with built-in change detection integration.

TS
// src/app/services/theme.service.ts
import { Injectable, signal, computed } from '@angular/core';

type Theme = 'light' | 'dark' | 'system';

@Injectable({ providedIn: 'root' })
export class ThemeService {
  // Writable signal — the source of truth
  private theme = signal<Theme>('system');

  // Computed signal — derived automatically
  readonly isDark = computed(() => {
    const t = this.theme();
    if (t === 'dark') return true;
    if (t === 'light') return false;
    return window.matchMedia('(prefers-color-scheme: dark)').matches;
  });

  // Read-only view
  readonly currentTheme = this.theme.asReadonly();

  setTheme(theme: Theme): void {
    this.theme.set(theme);
    localStorage.setItem('theme', theme);
  }

  toggleDark(): void {
    this.theme.update(t => (t === 'dark' ? 'light' : 'dark'));
  }
}

// In a component
@Component({
  selector: 'app-header',
  standalone: true,
  template: `
    <button (click)="themeService.toggleDark()">
      {{ themeService.isDark() ? '☀️' : '🌙' }}
    </button>
  `,
})
export class HeaderComponent {
  themeService = inject(ThemeService);
}
Tip
Signals integrate directly with Angular's change detection — no async pipe or subscribe needed in templates. This makes signal-based services significantly simpler than Observable-based ones.
Immutable State Updates

Regardless of which approach you use, always treat state as immutable — create new objects/arrays rather than mutating existing ones. This enables Angular's OnPush change detection to work correctly.

TS
// BAD — mutating state directly
addProduct(product: Product): void {
  this.products$.getValue().push(product); // mutates the array!
  // OnPush components won't detect this change
}

// GOOD — immutable update
addProduct(product: Product): void {
  const current = this.products$.getValue();
  this.products$.next([...current, product]); // new array reference
}

// GOOD — with signals
addProduct(product: Product): void {
  this.products.update(list => [...list, product]); // update() creates new array
}

// GOOD — nested object update
updateUserEmail(email: string): void {
  this.user.update(u => u ? { ...u, email } : null); // spread to copy
}
Choosing the Right Approach

Use this decision tree when choosing a state management approach:

  • State used by ONE component → keep it local (component property or signal)

  • State shared across a few components → service with BehaviorSubject or signals

  • Complex async flows with HTTP → service with BehaviorSubject + RxJS operators

  • Large team, audit trail, time-travel debugging → NgRx Store

  • Complex per-component state (e.g., paginated data table) → NgRx ComponentStore

  • Modern Angular 17+ with signal preference → NgRx SignalStore

State Management Anti-Patterns to Avoid
Warning
Avoid these common mistakes that lead to hard-to-debug applications.
  • Storing derived state — compute it with computed() or map() instead of storing it separately

  • Duplicating state — if the same data lives in two places they'll get out of sync

  • Mutating state objects directly — always use immutable updates

  • Putting HTTP calls in components — HTTP logic belongs in services

  • Subscribing inside subscribe — use switchMap/mergeMap instead

  • Not unsubscribing from services in components — causes memory leaks

Summary

Angular offers a spectrum of state management solutions. Start simple — local component state for UI-only state, services with BehaviorSubject or Signals for shared state — and only reach for NgRx when the complexity demands it. The most important principles are: single source of truth, immutable updates, and unidirectional data flow — all applicable regardless of which approach you choose.