3d cartoon hands holding a phone

Unlock full course by purchasing a membership

Lesson 3

State Management with RxJS and Signals

STANDARD

Why a State Management Approach Matters

You might hear that you don’t need to worry about state management in Angular. Common advice is to:

“Just use a service with a subject”

The modern Angular approach to this might be to use a service with a signal (instead of a Subject) — just like the examples we have already looked at:

import { Injectable, signal } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class PreferenceService {
  #showGreeting = signal(false);
  showGreeting = this.#showGreeting.asReadonly();

  toggleGreeting() {
    this.#showGreeting.update((showGreeting) => !showGreeting);
  }
}

Why worry about a “state management” approach when we can just do something simple like this? We can even extend this to deal with the asynchronous reactivity problem we talked about in the last lesson by including RxJS and observables.

Again, why worry about a “state management” approach?

The problem is that you already are using a state management approach. If you are managing state, and you don’t have an idea of specifically what your “state management approach” is, then your state management approach is just an ad-hoc state management approach that you make up on a case-by-case basis.

If you haven’t rigidly defined how you are going to approach state, then you might create a mess of different ideas dealing with different types of situations. It will also be especially hard to communicate to other developers how state should be managed — maybe some other developers on the project have slightly different ideas of how state should be managed.

The more experienced you are, the less of a concern this might be. People who have had a lot of experience with managing state may more “intuitively” be able to manage state without using some explicit system — but that intuition only comes from an ad-hoc state management system that has been developed over years and is deeply embedded into the person’s brain. This approach does not work so well in a team environment.

This is the benefit of explicitly deciding on a state management approach — whether that is one of your own design, or a library that handles these sorts of questions for you.

State Management with RxJS and Signals

In this course, we will be using an explicitly defined approach to state management that uses RxJS and Signals.

I think it is often a wise choice to use a state management library — as I just mentioned, the authors of the library (often brilliant engineers) have made a lot of the hard decisions for you and usually have tons of guides and documentation for you and your team to learn from.

However, for this course we will not be using a state management library. There are two main reasons for this.

  1. A big part of this course is learning the mechanisms of Angular and how everything works. State management libraries often make things more convenient, but they also hide implementation details. For the purposes of learning Angular and the concepts of state management, I think we would lose something by just using a state management library

  2. State management libraries often get a bad rap for being “too complicated”. Part of the problem is that state management libraries need to handle all (or at least most) situations for everyone. This often means there are lots of concepts, and abstractions, and features that you might not need. In the context of this course, I think this would detract from the goals.

Instead, I have broken down what I think are the key concepts of an ideal state management solution, and I have come up with a state management approach that is both simple but does not sacrifice on our goals of being as reactive and declarative as possible.

This is a bare bones but scalable approach that utilises the default features available to us, without complex abstractions, to manage state.

Although one of the primary goals in deciding on an approach for this course was to focus on what would provide the best learning experience, the result is actually a very nice approach to state management in general. I have been using this approach in all of my more recent applications and have found myself using it instead of state management libraries that I have heavily relied on in the past.

This approach borrows heavily from the NgRx/Redux style of using actions, reducers, and selectors. It is also inspired by the declarative approach of StateAdapt (a smaller, but very promising state management library).

If you are at all familiar with the Redux pattern then you will probably have an easy time understanding the pattern we will be using. If you aren’t familiar with the Redux pattern, then your learning here will serve a double purpose as you will also be able to understand the Redux pattern more easily if you come across it later.

We will discuss in detail how this approach works, but this supplementary video may help give you a high level overview. I originally published this video shortly after signals were first introduced to Angular, but even after all this time the core concepts remain unchanged:

How it works

We’ve already discussed a lot of the concepts that makes up this state management approach, so now we are really just coming up with some kind of formalised system of how to piece things together.

To help set the stage, this is a diagram of the general flow of how the state management approach we will be using works:

State management with RxJS and Signals

It shows how data flows into the application, how it flows through our state management process, how it eventually is consumed by our components, and also how the components can then interact with the state management flow.

I’ve also colour coded this diagram to indicate which parts of the state management process are handled by RxJS (red), which are handled by Signals (green), and where the transition between the two is.

Let’s break down each step, and see how it works with actual code. Keep in mind this code could either be directly in a component, or in a service that is provided to just one component, or a service that is shared among multiple components — the approach you use will depend on the situation, but the same basic principles will apply.

State

Our state is not really the start of this process, but it is hard for our state to have somewhere to flow to without first defining what that state looks like.

So, we typically begin by creating an interface that describes what our state looks like.

If we were to take the same example we looked at before where a checklist needed to be loaded from an API, we might define an interface like this:

interface ChecklistState {
  checklist: Checklist | null;
  loaded: boolean;
}

We want to keep track of two bits of state: the checklist itself (which will be null if it has not loaded yet), and a loaded flag that keeps track of whether the checklist has been loaded yet or not.

We can then use this interface when setting up our state signal:

  // state
  state = signal<ChecklistState>({
    checklist: null,
    loaded: false
  })

We create a signal for our state, supplying some initial values for the state we are interested in.

Sources

Everything begins at the source. This is where the data first flows into our application. It may flow in from outside of the application, but it may also flow in from inside of the application — kind of like water that has somehow been sent back up to the top of the waterfall.

Our sources will either be a standard observable, or a Subject. We will use observables if our source is something like an HTTP request to some backend — this is the situation where data is flowing in from outside of our application.

If we need to imperatively interact with the state in some way — for example we might have some user action like adding a comment that we need to trigger manually — then we will use a Subject. We can manually next a Subject at some point, and then we can react to that — either by deriving some new source from that action, or in the reducer step. This is the situation where data is flowing in from inside of our application. It is uglier, because it is imperative — water does not generally flow up the stream — it is, however, necessary at times.

Continuing with our checklist example, we might formally set this up like this:

  // state
  state = signal<ChecklistState>({
    checklist: null,
    loaded: false
  })

  // sources
  checklist$ = this.http.get<string>('someapi');

We have a source now, but without the rest of our process it doesn’t do anything — our state signal remains unchanged.

Reducers

The role of the reducer is to take the value from some event or action communicated through a source and determine how the current state should be updated as a result.

STANDARD
Key

Thanks for checking out the preview of this lesson!

You do not have the appropriate membership to view the full lesson. If you would like full access to this module you can view membership options (or log in if you are already have an appropriate membership).