DEV Community

Cover image for Using Angular Signals for Global State
Jamie Nordmeyer
Jamie Nordmeyer

Posted on • Originally published at jamienordmeyer.net on

Using Angular Signals for Global State

With the release of Angular 16 we got access to the new Signals API, along with a host of other features. They are currently in developer preview meaning that they could technically change between now and the next release of Angular. However, you can use them now, and they provide a much cleaner way to have reactive code in your web application. They DO not replace RxJS Observables; Angular services like HttpClient and resolvers still rely on RxJS. However, they do provide another tool in the box for responding to changes in your application that are often easier to understand than the RxJS alternative.

Code With RxJS

In an application that I’m working on, before Angular 16 shipped, I was using a custom StoreService to hold global application state. I’ve tried libraries like NgRx and Akita to manage global state, but found them to be way too heavy-handed for what I wanted (not saying ANYTHING negative towards these libraries; not every tool is right for every job, and the authors of these libraries would probably be the first to tell you that). This custom StoreService was created using RxJS, and looked like this:

import { Injectable } from '@angular/core';
import { ApplicationState } from '@shared/models';
import { UserCard } from '@shared/models';
import { BehaviorSubject, map } from 'rxjs';

const initialState: ApplicationState = {
  userCard: null,
};

@Injectable({
  providedIn: 'root',
})
export class StoreService {
  private readonly store$ = new BehaviorSubject<ApplicationState>(initialState);
  readonly userCard$ = this.store$.pipe(map((state) => state.userCard));

  setUserCard(userCard: UserCard | null) {
    this.store$.next({
      ...this.store$.value,
      userCard: userCard,
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Here, the UserCard interface represents the basic details of the logged-in user, things like name and data points for putting together a URL to their Avatar image. When I wanted to retrieve the user card for the logged-in user, a service would be used:

import { Inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { catchError, mergeMap, Observable, of, tap } from 'rxjs';
import { StoreService } from '@shared/services';
import { UserCard } from '@shared/models';

@Injectable({
  providedIn: 'root',
})
export class UserCardService {
  constructor(
    private http: HttpClient,
    private storeService: StoreService,
    @Inject('API_BASE_URL') private baseUrl: string
  ) {}

  getUserCard(): Observable<UserCard | null> {
    return this.storeService.userCard$.pipe(
      mergeMap((existingUserCard) =>
        existingUserCard
          ? of(existingUserCard)
          : this.http.get<UserCard>(`${this.baseUrl}v1/users/user-card`).pipe(
              tap((userCard) => this.storeService.setUserCard(userCard)),
              catchError(() => {
                this.storeService.setUserCard(null);
                return of(null);
              })
            )
      )
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

In the getUserCard method on this service, I first check to see if the StoreService has a previously loaded UserCard instance. If it does, I just return that. However, if the StoreService does not, it will make an HTPP call to retrieve the user card from the server, store that in the StoreService, and then return the user card.

For the component that displays the user card data, I would expose properties like this as public properties on the component:

  get avatarUrl$(): Observable<string | null> {
    return this.storeService.userCard$.pipe(
      map((uc) =>
        uc?.avatarVersionKey && uc.avatarVersionKey !== 0
          ? `${this.apiBaseUrl}v1/users/${uc?.uniqueKey}/avatar?size=64&v=${uc?.avatarVersionKey}`
          : `/assets/images/avatar_small.png`
      )
    );
  }

  get userFullName$(): Observable<string | null> {
    return this.storeService.userCard$.pipe(
      map((siu) => (siu ? `${siu.givenName} ${siu.surName}` : ''))
    );
  }
Enter fullscreen mode Exit fullscreen mode

Then in the HTML I need to use the async pipe to get the values out:

<span class="name">{{ userFullName$ | async }}</span>
Enter fullscreen mode Exit fullscreen mode

Coding With Signals

This of course works, and it’s what we’ve been doing for years in Angular. However, now that Signals have arrived, this code can be greatly simplified. Let’s start with the Signals-based implementation of the StoreService class.

import { Injectable, computed, signal } from '@angular/core';
import { ApplicationState } from '@shared/models';
import { UserCard } from '@shared/models';

const initialState: ApplicationState = {
  userCard: null,
};

@Injectable({
  providedIn: 'root',
})
export class StoreService {
  private readonly _store = signal(initialState);
  readonly userCard = computed(() => this._store().userCard);

  setUserCard(userCard: UserCard | null) {
    this._store.update((s) => ({ ...s, userCard: userCard }));
  }
}

Enter fullscreen mode Exit fullscreen mode

So far, it’s not a ton smaller or simplified from the RxJS version. Instead of a BehaviorSubject instance, I’m using a signal. The initial state is still being passed in to initialize the signal. I then define the userCard value to be a computed value. This creates a new signal, based on the _store signal in this case, that will automatically notify all of its listeners whenever the _store signal is updated, and return just the UserCard instance.

When calling setUserCard, I don’t need to call a next$ method. Calling next$ is an RxJS thing, and at least for me, it’s counter-intuitive. You have to remember that Observable is a stream of events and then next$ makes sense. However, with signals, I’m calling update, which feels more natural. The update method passes the current value of the signal to an arrow function, which I’m then spreading into a new object, and replacing the userCard value with the new UserCard. Currently, my state ONLY has the UserCard field, but as it starts to expand to hold other global state, then this becomes more useful.

The real “Oh…” moment for the cleanliness of the code for me comes in the new implementation of the UserCardService class:

import { Inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { catchError, Observable, of, tap } from 'rxjs';
import { StoreService } from '@shared/services';
import { UserCard } from '@shared/models';

@Injectable({
  providedIn: 'root',
})
export class UserCardService {
  constructor(
    private http: HttpClient,
    private storeService: StoreService,
    @Inject('API_BASE_URL') private baseUrl: string
  ) {}

  getUserCard(): Observable<UserCard | null> {
    const uc = this.storeService.userCard();
    if (uc) return of(uc);

    return this.http.get<UserCard>(`${this.baseUrl}v1/users/user-card`).pipe(
      tap((userCard) => this.storeService.setUserCard(userCard)),
      catchError(() => {
        this.storeService.setUserCard(null);
        return of(null);
      })
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

The getUserCard method still returns an Observable, as the resolver consuming it wants an observable. However, the implementation is much easier to understand in my opinion. I can execute the userCard method off the StoreService to get the current user card if there is one, and because it’s just a method, not an RxJS Observable, I can just return it straight away if I already have it; no need for mergeMap. I then call the HTTP endpoint like before, and add the user card to the StoreService.

The component fields are also greatly simplified (it’s really nice not having to use the RxJS pipe function):

  avatarUrl = computed(() => {
    const uc = this.storeService.userCard();
    return uc?.avatarVersionKey && uc.avatarVersionKey !== 0
      ? `${this.apiBaseUrl}v1/users/${uc?.uniqueKey}/avatar?size=64&v=${uc?.avatarVersionKey}`
      : `/assets/images/avatar_small.png`;
  });
  userFullName = computed(() => {
    const uc = this.storeService.userCard();
    return uc ? `${uc.givenName} ${uc.surName}` : '';
  });
Enter fullscreen mode Exit fullscreen mode

And now, in the HTML, we no longer need the async pipe. Instead, since each of these properties are signals, we access them like methods in the HTML:

<span class="name">{{ userFullName() }}</span>
Enter fullscreen mode Exit fullscreen mode

Final Thoughts

I really like the new Signals API, and am looking forward to seeing where else the Angular team takes it. Will the HttpClient class eventually use Signals (maybe via a new class called HttpSignalsClient to maintain backward compatibility)? Will there be new constructs for our HTML templates that are Signals aware? Only time will tell. But so far, I really like what I’m seeing.

Top comments (0)