Angular State Management - JU-DEV-Bootcamps/ERAS GitHub Wiki
- Simple Service-Based State Management
The most basic approach to state management in Angular uses services with observables.
import { Injectable } from "@angular/core";
import { BehaviorSubject } from "rxjs";
@Injectable({
providedIn: "root",
})
export class MyService {
private data$ = new BehaviorSubject<any>(null);
setData(data: any) {
this.data$.next(data);
}
getData() {
return this.data$.asObservable();
}
}
Using the service in a component:
import { Component } from "@angular/core";
import { MyService } from "./my.service";
@Component({
selector: "app-root",
template: ` <app-child [data]="data$ | async"></app-child> `,
})
export class AppComponent {
data$ = this.myService.getData();
constructor(private myService: MyService) {}
}
- Angular Signals Implementation
Signals, introduced in Angular 16+, provide a new way to handle reactive state management.
import { Injectable } from "@angular/core";
import { Signal } from "@angular/core";
import { computed } from "@angular/core";
@Injectable({
providedIn: "root",
})
export class MyService {
data = Signal<any>(null);
setData(data: any) {
this.data.set(data);
}
getData() {
return this.data;
}
}
Using Signals n a component:
import { Component } from "@angular/core";
import { MyService } from "./my.service";
@Component({
selector: "app-root",
template: ` <app-child [data]="data()"></app-child> `,
})
export class AppComponent {
data = computed(() => this.myService.getData());
constructor(private myService: MyService) {}
}
- NgRx: Enterprise-Level State Management
NgRx provides a robust state management solution based on Redux principles.
First, install NgRx:
npm install @ngrx/store @ngrx/effects @ngrx/entity @ngrx/store-devtools
Implementation:
import { createAction, props } from "@ngrx/store";
export const loadTodos = createAction("[Todos] Load Todos");
export const loadTodosSuccess = createAction(
"[Todos] Load Todos Success",
props<{ todos: Todo[] }>(),
);
export const loadTodosFailure = createAction(
"[Todos] Load Todos Failure",
props<{ error: any }>(),
);
import { createReducer, on } from "@ngrx/store";
import * as TodoActions from "./todo.actions";
import { EntityState, createEntityAdapter } from "@ngrx/entity";
export interface TodoState extends EntityState<Todo> {
loading: boolean;
error: any;
}
export const todoAdapter = createEntityAdapter<Todo>();
export const initialState: TodoState = todoAdapter.getInitialState({
loading: false,
error: null,
});
export const todoReducer = createReducer(
initialState,
on(TodoActions.loadTodos, (state) => ({
...state,
loading: true,
error: null,
})),
on(TodoActions.loadTodosSuccess, (state, { todos }) =>
todoAdapter.setAll(todos, { ...state, loading: false }),
),
on(TodoActions.loadTodosFailure, (state, { error }) => ({
...state,
loading: false,
error,
})),
);
import { Injectable } from "@angular/core";
import { Actions, createEffect, ofType } from "@ngrx/effects";
import { of } from "rxjs";
import { map, mergeMap, catchError } 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 }))),
),
),
),
);
constructor(
private actions$: Actions,
private todoService: TodoService,
) {}
}
import { createFeatureSelector, createSelector } from "@ngrx/store";
import { TodoState, todoAdapter } from "./todo.reducer";
export const selectTodoState = createFeatureSelector<TodoState>("todos");
export const {
selectIds: selectTodoIds,
selectEntities: selectTodoEntities,
selectAll: selectAllTodos,
selectTotal: selectTotalTodos,
} = todoAdapter.getSelectors(selectTodoState);
export const selectLoading = createSelector(
selectTodoState,
(state) => state.loading,
);
export const selectError = createSelector(
selectTodoState,
(state) => state.error,
);
Using NgRx in a component:
import { component, OnInit } from "@angular/core";
import { Store } from "@ngrx/store";
import * TodoActions from "./store/todo.actions"
import * TodoSelectors from "./store/todo.selectors"
@Component({
selector: 'app-todo-list',
template: `
<div *ngIf="loading$ | async">Loading...</div>
<div *ngIf="error$ | async as error">Error: {{ error }}</div>
<ul>
<li *ngFor="let todo of todos$ | async">
{{ todo.title }}
<input
type="checkbox"
[checked]="todo.completed"
(change)="toggleTodo(todo.id)"
>
</li>
</ul>
`
})
export class TodoListComponent implements OnInit {
todos$ = this.store.select(TodoSelectors.selectAllTodos);
loading$ = this.store.select(TodoSelectors.selectLoading);
error$ = this.store.select(TodoSelectors.selectError);
constructor(private store: Store) {}
ngOnInit() {
this.store.dispatch(TodoActions.loadTodos());
}
toggleTodo(id: number) {
// Implement toggle action
}
}
Angular offers multiple approaches to state management, each with its own strengths:
- Services with Observables: Simple and effective for small applications
- Signals: Modern, performant solution for reactive state management
- NgRx: Robust, scalable solution for large applications