Simplifying State Management in Angular with Signals and LocalStorage

State management is a crucial aspect of building robust and scalable web applications. It involves handling and synchronizing the application’s state across various components. Angular, being a popular front-end framework, offers several ways to manage state, such as using services, observables, and third-party libraries like NgRx. In this blog, we’ll explore a simple yet effective approach to state management using Angular services, signals, and LocalStorage.

Bellow is example of state management using NgRx

Understanding the Approach of state management using Signals

In this state management approach, we’ll use two service files: app-state.service.ts and signal.service.ts. The AppStateService will be responsible for managing and retrieving application states, while the SignalService will handle the signaling mechanism between components. The states will be stored both in memory and in the browser’s LocalStorage.

Let’s dive into the code and understand each part step by step.

The SignalService (signal.service.ts)

The SignalService is responsible for managing signals that can notify subscribers whenever there is a change in a particular state. It uses RxJS’s Subject and BehaviorSubject to accomplish this.

// signal.service.ts

import { Injectable } from '@angular/core';
import { BehaviorSubject, Subject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class SignalService {
  private signals: { [key: string]: Subject<any> } = {};

  // Dispatch a signal to notify subscribers of a state change
  dispatchSignal(signalName: string, data?: any) {
    if (this.signals[signalName]) {
      this.signals[signalName].next(data);
    }
  }

  // Update the value of a signal (or create a new one if not exists)
  updateSignalValue(key: string, newValue: string): void {
    const signal = this.signals[key];
    if (!signal) {
      this.signals[key] = new BehaviorSubject<string>(newValue);
    } else {
      signal.next(newValue);
    }
  }

  // Subscribe to a signal to receive state change notifications
  subscribeToSignal(signalName: string): Subject<any> {
    let signal = this.signals[signalName];
    if (!signal) {
      signal = new Subject<any>();
      this.signals[signalName] = signal;
    }
    return signal;
  }
}

The AppStateService (app-state.service.ts)

The AppStateService is responsible for managing application states and interacting with the SignalService for state change notifications. It also uses LocalStorage to persist the states, ensuring the state is retained even if the user refreshes the page or navigates away.

// app-state.service.ts

import { Injectable } from '@angular/core';
import { SignalService } from './signal.service';
import { Observable, of } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class AppStateService {
  private states: { [key: string]: any } = {};

  constructor(private signalService: SignalService) {
    this.loadStatesFromLocalStorage();
  }

  // Load states from LocalStorage during service initialization
  private loadStatesFromLocalStorage() {
    const savedStates = localStorage.getItem('appStates');
    if (savedStates) {
      this.states = JSON.parse(savedStates);
      for (const key in this.states) {
        if (this.states.hasOwnProperty(key)) {
          const value = this.states[key];
          this.signalService.updateSignalValue(key, value); 
        }
      } 
    }
  }

  // Get the state with a given key from the SignalService or LocalStorage
  getState(key: string): Observable<string> {
    const fromSignal = this.signalService.subscribeToSignal(key);
    const fromLocal = localStorage.getItem(key);

    return fromSignal || of(fromLocal || '');
  }

  // Set the state with a given key and value
  setState<T>(key: string, newState: T) {
    this.states[key] = newState;
    this.saveStatesToLocalStorage();
    this.signalService.dispatchSignal(key, newState);
  }

  // Save the current states to LocalStorage
  private saveStatesToLocalStorage() {
    localStorage.setItem('appStates', JSON.stringify(this.states));
  }
}

Putting It All Together

With these two services in place, you can now manage your application states efficiently. Let’s see how you can use them in your components:

1: Inject the AppStateService in your components.

import { Component, OnInit } from '@angular/core';
import { AppStateService } from './app-state.service';

@Component({
  selector: 'app-your-component',
  templateUrl: './your-component.component.html',
  styleUrls: ['./your-component.component.css']
})
export class YourComponent implements OnInit {
  stateValue: string;

  constructor(private appStateService: AppStateService) { }

  ngOnInit() {
    // Subscribe to the state with key 'yourStateKey'
    this.appStateService.getState('yourStateKey').subscribe((value) => {
      this.stateValue = value;
    });
  }

  // Update the state with key 'yourStateKey' when needed
  updateState() {
    const newValue = 'New State Value';
    this.appStateService.setState('yourStateKey', newValue);
  }
}

By using this approach, you have a centralized way to manage your application states, and the components can interact with the states without direct coupling. Any change to the state triggers notifications to all subscribed components, ensuring data consistency throughout your application.

Conclusion

In this blog, we explored a simple and effective approach to state management in Angular using services, signals, and LocalStorage. By adopting this approach, you can maintain a clear separation of concerns and ensure a more organized and scalable codebase. Additionally, the use of LocalStorage allows the application to persist states across page reloads, enhancing the user experience.

Remember that this is just one of many state management solutions in Angular, and the best approach depends on the complexity and specific requirements of your application. However, the combination of services, signals, and LocalStorage offers a great starting point for many projects. Happy coding!

Leave a comment