đź‘‹ Hey there!

I'm Josh Morony, and this website is a collection of my free content centered around creating modern Angular applications using the latest Angular features, and concepts like reactive and declarative code.

Tutorial hero
Lesson icon

A New Reactive And Declarative Approach to Angular State Management

Originally published July 05, 2023 Time 8 mins

As soon as I could get my hands on signals I have been playing with them, and since then I’ve been trying to figure out what my “default” approach to state management in this new Angular world would be.

By “default” I specifically mean how I would go about managing state with just plain Angular, using no 3rd party libraries or any complicated boilerplate.

Mostly I want it to be reactive, declarative, and easy enough to teach to beginners.

This article is adapted from the following video:

The Declarative Approach

My favourite pattern for state management at the moment is the declarative approach implemented by the State Adapt library - I have already done a video on that which you can find here.

But the basic idea is that we have sources of data at the top of our state management flow. Whenever anything in the application changes it will be represented by one of these sources.

We might have a source that loads data from an API which will emit when that data loads. Or, we might have a source that represents a new item being added by the user, and when that particular action occurs that source will emit the data.

source that represents a new item being added by the user

Then our state is updated in response to these sources emitting data, and then the rest of our application can react to this state changing. With this approach, we don’t have to have any imperative code manually updating things in multiple places, we just trigger the appropriate data source whenever we need to change something.

Why not Third-Party Libraries?

Unfortunately, I can’t just use State Adapt or NgRx or some other library as a default. Primarily because I create a lot of teaching material, and I don’t think it makes sense to teach a 3rd party library as the default way to do something in Angular. Plus, I think it’s good to understand how to do these things without libraries anyway.

So, I wanted to arrive at a pattern that captures a lot of the same general ideas as something like State Adapt, but I also wanted something that is reasonably straightforward to follow for people who aren’t into this whole declarative code, FRP, RxJS thing - and not require a whole bunch of boilerplate. If I end up just basically creating another state management library it defeats the whole purpose.

I ended up with a result that combines signals and RxJS that I think is actually really nice and achieves the goals I wanted, with just some minor compromises. So, let’s talk about it.

Establishing the State

Once again, I’ve refactored my quicklists application for the 105th time now. Again - the full source code can be accessed here if you want to check out the entire app - we are just going to primarily focus on one section of the app.

Ok, so we have our state and initial state - nothing surprising here. The state is held in a signal, and then we have “selectors” that allow our application to consume parts of this state - these are just computed signals derived from the main state.

export interface ChecklistsState {
  checklists: Checklist[];
  loaded: boolean;
  error: string | null;
}

@Injectable({
  providedIn: "root",
})
export class ChecklistService {
  private checklistItemService = inject(ChecklistItemService);
  private storageService = inject(StorageService);

  // state
  private state = signal<ChecklistsState>({
    checklists: [],
    loaded: false,
    error: null,
  });

  // selectors
  checklists = computed(() => this.state().checklists);
  loaded = computed(() => this.state().loaded);
  error = computed(() => this.state().error);

Understanding Data Sources

Now we get into our sources. In this case, we have four sources of data, an observable from the storage service that loads the data - importantly this could potentially error which we need to handle. And we also have add and edit subjects that will emit whenever a checklist is being added or edited, and a remove subject which is actually derived from a data source in another service.

  // sources
  private checklistsLoaded$ = this.storageService.loadChecklists();
  add$ = new Subject<AddChecklist>();
  edit$ = new Subject<EditChecklist>();
  remove$ = this.checklistItemService.checklistRemoved$;

This means that when the checklistRemoved source from the other service is triggered, our state in this service can react to that same source.

So, the state in this service will only ever change when one of these data sources emits.

checklistRemoved source from the other service is triggered

But we still need to deal with how to update the state when one of these “sources” or “actions” - if you want to call them that - emits.

This is where we get into the part of this approach that required some minor compromises. Using a 3rd party library would help hide some of this kind of boilerplate, but it’s not too bad.

Basically, we subscribe to each of the source streams and specify how the state signal should be updated in response to any of the source stream emitting.

how state signal should be updated in response to any source stream emitting

For example, when the checklistsLoaded source emits we should take the data from that emission, update the checklists array with that data, and set loaded to true. If the load fails, we set the error instead.

  constructor() {
    // reducers
    this.checklistsLoaded$.pipe(takeUntilDestroyed()).subscribe({
      next: (checklists) =>
      this.state.update((state) => ({
        ...state,
        checklists,
        loaded: true,
      })),
      error: (err) => this.state.update((state) => ({...state, error: err}))
    });

We do this for each source. We subscribe to the source and specify how the state should be updated in response to that source emitting.

Addressing the Reactive Approach

Now before you call me a heretic for claiming to be reactive and declarative whilst dropping subscribes all over the place, hear me out.

There were basically two other options here whilst working within the parameters I specified. Instead of source streams, I could have instead had callback functions like this:

update the state values without requiring a subscription

This would allow me to update the state values without requiring a subscription.

Whilst it’s nice to not have the subscribe, this code isn’t any more declarative than the code with the subscribe - it is still imperatively setting the subject.

But the major downside of this approach is that no other parts of the application can react to this callback method being called. Take this remove source for example.

  // sources
  private checklistsLoaded$ = this.storageService.loadChecklists();
  add$ = new Subject<AddChecklist>();
  edit$ = new Subject<EditChecklist>();
  remove$ = this.checklistItemService.checklistRemoved$;

It reacts to the checklistRemoved$ source from another service. If I were to instead use a checklistRemoved callback function in the other service, then I would need to do something like this (image below) to make sure my other service gets notified about this as well.

checklistRemoved callback function in the other service

This is far more imperative and involves this sort of double handling where we need to make sure we are updating things in multiple places rather than reacting to a single source emitting.

Another approach would be to have everything derived entirely from the data sources using the scan operator. This would be technically more declarative, but it is a whole lot harder to follow if you aren’t very familiar with these concepts. Besides, even as someone who is familiar with these concepts I generally don’t really like the scan approach anyway.

derived entirely from the data sources using the scan operator

So, I landed on just subscribing like this - source code here - it is actually more declarative than using callbacks, and especially now with Angular’s new takeUntilDestroy pipe it makes the cleanup of subscriptions nice and easy.

this.remove$.pipe(takeUntilDestroyed()).subscribe((id) =>
  this.state.update((state) => ({
    ...state,
    checklists: state.checklists.filter((checklist) => checklist.id !== id),
  }))
);

Handling Side Effects

Also, side effects shouldn’t be needed that often, but they are quite easy with this approach. Here I am just using effect from the Signals API to save a copy of the data into local storage whenever the checklists state changes.

// effects
effect(() => {
  if (this.loaded()) {
    this.storageService.saveChecklists(this.checklists());
  }
});

You could trigger whatever kind of imperative code here by reacting to that signal changing.

Conclusion

There are still potentially some optimisations here, especially around using multiple signals for the state to better take advantage of change detection changes coming with signal based components. But, overall, I am pretty happy with the general concept here.

Learn to build modern Angular apps with my course