Angular State Management
Angular provides multiple approaches to state management: services with RxJS, NgRx (Redux pattern), and Signals (Angular 16+). Choose based on your application complexity.
Service-Based State (Simple)
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
interface AppState {
user: User | null;
isLoading: boolean;
error: string | null;
}
@Injectable({ providedIn: 'root' })
export class StateService {
private state: AppState = {
user: null,
isLoading: false,
error: null
};
private stateSubject = new BehaviorSubject<AppState>(this.state);
public state$ = this.stateSubject.asObservable();
// Selectors
get user$(): Observable<User | null> {
return this.state$.pipe(map(state => state.user));
}
get isLoading$(): Observable<boolean> {
return this.state$.pipe(map(state => state.isLoading));
}
// Actions
setUser(user: User) {
this.state = { ...this.state, user };
this.stateSubject.next(this.state);
}
setLoading(isLoading: boolean) {
this.state = { ...this.state, isLoading };
this.stateSubject.next(this.state);
}
setError(error: string) {
this.state = { ...this.state, error };
this.stateSubject.next(this.state);
}
}NgRx (Redux Pattern)
NgRx is a Redux-inspired state management library for Angular. It provides a predictable state container with actions, reducers, effects, and selectors.
// 1. Define State
export interface TodoState {
todos: Todo[];
loading: boolean;
error: string | null;
}
export const initialState: TodoState = {
todos: [],
loading: false,
error: null
};
// 2. Create Actions
import { createAction, props } from '@ngrx/store';
export const loadTodos = createAction('[Todo] Load Todos');
export const loadTodosSuccess = createAction(
'[Todo] Load Todos Success',
props<{ todos: Todo[] }>()
);
export const loadTodosFailure = createAction(
'[Todo] Load Todos Failure',
props<{ error: string }>()
);
export const addTodo = createAction(
'[Todo] Add Todo',
props<{ todo: Todo }>()
);
export const deleteTodo = createAction(
'[Todo] Delete Todo',
props<{ id: string }>()
);
// 3. Create Reducer
import { createReducer, on } from '@ngrx/store';
export const todoReducer = createReducer(
initialState,
on(loadTodos, (state) => ({
...state,
loading: true,
error: null
})),
on(loadTodosSuccess, (state, { todos }) => ({
...state,
todos,
loading: false
})),
on(loadTodosFailure, (state, { error }) => ({
...state,
error,
loading: false
})),
on(addTodo, (state, { todo }) => ({
...state,
todos: [...state.todos, todo]
})),
on(deleteTodo, (state, { id }) => ({
...state,
todos: state.todos.filter(t => t.id !== id)
}))
);
// 4. Create Selectors
import { createFeatureSelector, createSelector } from '@ngrx/store';
export const selectTodoState = createFeatureSelector<TodoState>('todos');
export const selectAllTodos = createSelector(
selectTodoState,
(state) => state.todos
);
export const selectActiveTodos = createSelector(
selectAllTodos,
(todos) => todos.filter(t => !t.completed)
);
export const selectCompletedTodos = createSelector(
selectAllTodos,
(todos) => todos.filter(t => t.completed)
);
export const selectTodosLoading = createSelector(
selectTodoState,
(state) => state.loading
);NgRx Effects
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { of } from 'rxjs';
import { map, mergeMap, catchError, tap } from 'rxjs/operators';
import * as TodoActions from './todo.actions';
@Injectable()
export class TodoEffects {
loadTodos$ = createEffect(() =>
this.actions$.pipe(
ofType(TodoActions.loadTodos),
mergeMap(() =>
this.todoService.getTodos().pipe(
map(todos => TodoActions.loadTodosSuccess({ todos })),
catchError(error =>
of(TodoActions.loadTodosFailure({ error: error.message }))
)
)
)
)
);
addTodo$ = createEffect(() =>
this.actions$.pipe(
ofType(TodoActions.addTodo),
mergeMap(action =>
this.todoService.createTodo(action.todo).pipe(
map(todo => TodoActions.addTodoSuccess({ todo })),
catchError(error => of(TodoActions.addTodoFailure({ error })))
)
)
)
);
// Non-dispatching effect (for side effects only)
logActions$ = createEffect(
() =>
this.actions$.pipe(
tap(action => console.log('Action:', action))
),
{ dispatch: false }
);
constructor(
private actions$: Actions,
private todoService: TodoService
) { }
}Using NgRx in Components
import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import * as TodoActions from './store/todo.actions';
import * as TodoSelectors from './store/todo.selectors';
@Component({
selector: 'app-todo-list',
template: `
<div>
<h2>Todos</h2>
<div *ngIf="loading$ | async">Loading...</div>
<ul>
<li *ngFor="let todo of todos$ | async">
{{ todo.text }}
<button (click)="delete(todo.id)">Delete</button>
</li>
</ul>
<button (click)="load()">Load Todos</button>
</div>
`
})
export class TodoListComponent implements OnInit {
todos$: Observable<Todo[]>;
loading$: Observable<boolean>;
constructor(private store: Store) {
this.todos$ = this.store.select(TodoSelectors.selectAllTodos);
this.loading$ = this.store.select(TodoSelectors.selectTodosLoading);
}
ngOnInit() {
this.load();
}
load() {
this.store.dispatch(TodoActions.loadTodos());
}
delete(id: string) {
this.store.dispatch(TodoActions.deleteTodo({ id }));
}
}Signals (Angular 16+)
Signals provide a reactive primitive for managing state with fine-grained reactivity and better performance than Zone.js.
import { Component, signal, computed, effect } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<div>
<p>Count: {{ count() }}</p>
<p>Double: {{ doubleCount() }}</p>
<button (click)="increment()">+</button>
<button (click)="decrement()">-</button>
<button (click)="reset()">Reset</button>
</div>
`
})
export class CounterComponent {
// Signal - writable
count = signal(0);
// Computed signal - derived
doubleCount = computed(() => this.count() * 2);
// Effect - side effects
constructor() {
effect(() => {
console.log('Count changed:', this.count());
localStorage.setItem('count', this.count().toString());
});
}
increment() {
this.count.update(value => value + 1);
}
decrement() {
this.count.update(value => value - 1);
}
reset() {
this.count.set(0);
}
}
// Signal-based service
import { Injectable, signal, computed } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class TodoSignalService {
private todos = signal<Todo[]>([]);
// Public readonly version
readonly allTodos = this.todos.asReadonly();
// Computed signals
readonly activeTodos = computed(() =>
this.todos().filter(t => !t.completed)
);
readonly completedTodos = computed(() =>
this.todos().filter(t => t.completed)
);
readonly stats = computed(() => ({
total: this.todos().length,
active: this.activeTodos().length,
completed: this.completedTodos().length
}));
addTodo(todo: Todo) {
this.todos.update(todos => [...todos, todo]);
}
deleteTodo(id: string) {
this.todos.update(todos => todos.filter(t => t.id !== id));
}
toggleTodo(id: string) {
this.todos.update(todos =>
todos.map(t =>
t.id === id ? { ...t, completed: !t.completed } : t
)
);
}
}Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free