Angular State Management - JU-DEV-Bootcamps/ERAS GitHub Wiki

Angular State Management

  1. 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) {}
}
  1. 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) {}
}
  1. NgRx: Enterprise-Level State Management

NgRx provides a robust state management solution based on Redux principles. image

Setting up NgRx:

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
  }
}

Conclusion

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
⚠️ **GitHub.com Fallback** ⚠️