Angular State Management
State management organizes how data changes over time.
State Management Essentials
- Local first: Start with component signals; promote to a service (store) only when sharing is needed.
- Signals: Use
signal()
for local and service-backed state; derive withcomputed()
. - Interop: Use RxJS only when stream semantics are required; keep global state minimal.
import { Injectable, signal, computed, inject } from '@angular/core';
@Injectable({ providedIn: 'root' })
class CounterStore {
count = signal(0);
double = computed(() => this.count() * 2);
inc() { this.count.update(n => n + 1); }
}
// Component: const store = inject(CounterStore);
Notes:
- Related: See Signals, Services, and Change Detection.
- Start with a simple service + signals; reach for libraries only when complexity demands it.
Service-backed Signals (Store)
Lift state into a service to share across components.
It improves reuse.
import { Injectable, signal, inject } from '@angular/core';
@Injectable({ providedIn: 'root' })
class CounterStore {
count = signal(0);
inc() { this.count.update(n => n + 1); }
}
// Component usage
class App { store = inject(CounterStore); }
Example
import { bootstrapApplication } from '@angular/platform-browser';
import { Component, Injectable, signal, inject } from '@angular/core';
@Injectable({ providedIn: 'root' })
class CounterStore {
count = signal(0);
inc() { this.count.update(n => n + 1); }
}
@Component({
selector: 'app-root',
standalone: true,
template: `
<h3>Service with Signals</h3>
<p>Count: {{ store.count() }}</p>
<button (click)="store.inc()">Increment</button>
`
})
class App { store = inject(CounterStore); }
bootstrapApplication(App);
<app-root></app-root>
Example explained
CounterStore
: A service holding asignal
and an update method (inc()
).inject(CounterStore)
: Retrieves the store in the component (no constructor required).store.count()
: Read the current value in the template; clicking the button callsstore.inc()
.
Design tips:
- Keep a single source of truth per feature in a service; inject where needed.
- Expose methods for updates; avoid mutating state from components directly.
- Derive values with computed signals; keep side effects (like persistence) in the service.
- Bridge to streams only when needed (e.g., to interop with RxJS-based APIs).
Local vs Global State
- Keep most state local to components to reduce coupling.
- Promote to a shared service only when multiple features need it.
- Scope providers at the feature/route level when isolation is desired.
import { provideRouter, Routes } from '@angular/router';
const routes: Routes = [
{ path: '', component: Home, providers: [CounterStore] }
];
bootstrapApplication(App, { providers: [provideRouter(routes)] });
Guidelines:
- Promote state when multiple routes/components need the same data or when caching improves UX.
- Separate UI state (filters, dialogs) from server/cache state; manage them independently.
- Initialize lazily on first use and consider reset points (e.g., on logout).