DEV Community

Cover image for Angular State Management: A Comparison of the Different Options Available
chintanonweb
chintanonweb

Posted on • Edited on

Angular State Management: A Comparison of the Different Options Available

Angular State Management: A Complete Guide to Different Approaches

State management is a crucial aspect of modern Angular applications. As applications grow in complexity, managing state effectively becomes increasingly important. In this comprehensive guide, we'll explore different state management options in Angular, from simple services to sophisticated solutions like NgRx, including the new Signals feature.

Introduction to State Management

State management refers to how we handle and maintain data across components in our application. This data could include:

  • User information
  • Application settings
  • Form data
  • API responses
  • UI state (loading indicators, toggle states)

Let's dive into different approaches, starting from the simplest to more complex solutions.

1. Simple Service-Based State Management

The most basic approach to state management in Angular uses services with observables.

Implementation Example

First, let's create a basic state service:

// user-state.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';

interface User {
  id: number;
  name: string;
  email: string;
}

@Injectable({
  providedIn: 'root'
})
export class UserStateService {
  private userSubject = new BehaviorSubject<User | null>(null);
  user$ = this.userSubject.asObservable();

  setUser(user: User) {
    this.userSubject.next(user);
  }

  clearUser() {
    this.userSubject.next(null);
  }

  getCurrentUser(): User | null {
    return this.userSubject.getValue();
  }
}
Enter fullscreen mode Exit fullscreen mode

Using the service in a component:

// user-profile.component.ts
import { Component, OnInit } from '@angular/core';
import { UserStateService } from './user-state.service';

@Component({
  selector: 'app-user-profile',
  template: `
    <div *ngIf="user$ | async as user">
      <h2>Welcome, {{ user.name }}!</h2>
      <p>Email: {{ user.email }}</p>
    </div>
  `
})
export class UserProfileComponent implements OnInit {
  user$ = this.userState.user$;

  constructor(private userState: UserStateService) {}

  ngOnInit() {
    // Simulating user login
    this.userState.setUser({
      id: 1,
      name: 'John Doe',
      email: 'john@example.com'
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

2. Angular Signals: The New State Management Approach

Signals, introduced in Angular 16+, provide a new way to handle reactive state management.

Basic Signals Implementation

// counter.service.ts
import { Injectable, signal, computed } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class CounterService {
  // Create a signal with initial value
  private count = signal(0);

  // Create a computed signal
  doubleCount = computed(() => this.count() * 2);

  increment() {
    // Update signal value
    this.count.update(current => current + 1);
  }

  decrement() {
    this.count.update(current => current - 1);
  }

  getCount() {
    return this.count;
  }
}
Enter fullscreen mode Exit fullscreen mode

Using Signals in a component:

// counter.component.ts
import { Component } from '@angular/core';
import { CounterService } from './counter.service';

@Component({
  selector: 'app-counter',
  template: `
    <div>
      <h2>Counter: {{ counterService.getCount()() }}</h2>
      <h3>Double Count: {{ counterService.doubleCount() }}</h3>
      <button (click)="counterService.increment()">Increment</button>
      <button (click)="counterService.decrement()">Decrement</button>
    </div>
  `
})
export class CounterComponent {
  constructor(public counterService: CounterService) {}
}
Enter fullscreen mode Exit fullscreen mode

Advanced Signals Example with API Integration

// todo.service.ts
import { Injectable, signal, computed } from '@angular/core';
import { HttpClient } from '@angular/common/http';

interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

@Injectable({
  providedIn: 'root'
})
export class TodoService {
  private todos = signal<Todo[]>([]);
  private loading = signal(false);

  // Computed signals
  completedTodos = computed(() => 
    this.todos().filter(todo => todo.completed)
  );

  pendingTodos = computed(() => 
    this.todos().filter(todo => !todo.completed)
  );

  constructor(private http: HttpClient) {}

  async fetchTodos() {
    this.loading.set(true);
    try {
      const response = await fetch('https://api.example.com/todos');
      const data = await response.json();
      this.todos.set(data);
    } finally {
      this.loading.set(false);
    }
  }

  addTodo(title: string) {
    const newTodo: Todo = {
      id: Date.now(),
      title,
      completed: false
    };
    this.todos.update(current => [...current, newTodo]);
  }

  toggleTodo(id: number) {
    this.todos.update(todos =>
      todos.map(todo =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

3. NgRx: Enterprise-Level State Management

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

Setting up NgRx

First, install NgRx:

npm install @ngrx/store @ngrx/effects @ngrx/entity @ngrx/store-devtools
Enter fullscreen mode Exit fullscreen mode

Implementation Example

// todo.actions.ts
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: any }>()
);
Enter fullscreen mode Exit fullscreen mode
// todo.reducer.ts
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
  })),
  on(TodoActions.loadTodosSuccess, (state, { todos }) =>
    todoAdapter.setAll(todos, { ...state, loading: false })
  ),
  on(TodoActions.loadTodosFailure, (state, { error }) => ({
    ...state,
    error,
    loading: false
  }))
);
Enter fullscreen mode Exit fullscreen mode
// todo.effects.ts
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
  ) {}
}
Enter fullscreen mode Exit fullscreen mode
// todo.selectors.ts
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { TodoState, todoAdapter } from './todo.reducer';

export const selectTodoState = createFeatureSelector<TodoState>('todos');

export const {
  selectAll: selectAllTodos,
  selectEntities: selectTodoEntities,
  selectIds: selectTodoIds,
} = todoAdapter.getSelectors(selectTodoState);

export const selectLoading = createSelector(
  selectTodoState,
  state => state.loading
);

export const selectError = createSelector(
  selectTodoState,
  state => state.error
);
Enter fullscreen mode Exit fullscreen mode

Using NgRx in a component:

// todo-list.component.ts
import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import * as TodoActions from './store/todo.actions';
import * as 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
  }
}
Enter fullscreen mode Exit fullscreen mode

Best Practices and Recommendations

  1. Choose Based on Application Size:

    • Small apps: Services with observables
    • Medium apps: Signals
    • Large apps: NgRx
  2. State Structure:

    • Keep state normalized
    • Avoid duplicate data
    • Consider using immutable patterns
  3. Performance Considerations:

    • Use selectors for derived state
    • Implement proper memoization
    • Avoid unnecessary state updates

Frequently Asked Questions

Q: When should I use Signals instead of Services with BehaviorSubject?
A: Use Signals when you need fine-grained reactivity and better performance. They're especially useful for UI state that changes frequently.

Q: Is NgRx overkill for small applications?
A: Yes, for small applications, NgRx can add unnecessary complexity. Start with services or signals and migrate to NgRx when needed.

Q: Can I mix different state management approaches?
A: Yes, you can use different approaches for different parts of your application. For example, use Signals for UI state and NgRx for complex domain state.

Q: How do Signals compare to RxJS Observables?
A: Signals are simpler and more performant for straightforward state management, while RxJS Observables offer more powerful data transformation and combination operators.

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

Choose the approach that best fits your application's needs, considering factors like team size, application complexity, and scalability requirements.

Top comments (3)

Collapse
 
fyodorio profile image
Fyodor

The signals also slowly becoming a thing in this regard

Collapse
 
carniatto profile image
Mateus Carniatto

I think you've missed NGXS there.

Collapse
 
nigrosimone profile image
Nigro Simone

Try npmjs.com/package/ng-simple-state is a simple state management with only Services and RxJS or Signal.