đź‘‹ 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

An Introduction to State Adapt for Declarative Code in Angular

Originally published June 07, 2023 Time 8 mins

The creator of State Adapt, Mike Pearson, is one of the most passionate supporters of declarative code in Angular.

For those new to the concept, the most important concepts to understand are that, with declarative code, you can understand what something is and how it changes just by looking at its declaration - it is not imperatively changed after its declaration. If you aren’t too familiar, I have a video on declarative code which you can access here.

Declarative data flow

We generally have this architecture where we can visualise data flowing into the application from the top, and it follows this downward flow where everything automatically reacts to changes in the dependencies above it. This is where the “reactive” aspect comes into play, the reactions happen automatically we don’t need to manually update things like we would with imperative code, it all just happens, by definition, based on the things declaration.

This article is based on the following video:

Introduction to State Adapt

So, what exactly is State Adapt and why does it exist? In short, it’s Mike’s attempt at creating a state management library that is as declarative as possible.

A Real-World Example

Let’s dive straight into an example. I refactored the quicklists application to demonstrate how it works. For those who what to dive deeper into the Quicklist application I have linked the broader application here.

The focus here is mainly on one feature of this application - a service that manages checklist items, and a component that makes use of this service.

<app-checklist-item-list [checklistItem]="items()" (toggle)="cis.toggle$.next($event)" (delete)="cis.remove$.next($event)" (edit)="checklistItemBeingEdited.set($event)" />

Key Parts

The first key part is our data sources, which is how data flows into our application.

These data sources can be regular observable streams like the checklistItemsLoaded stream that is responsible for loading the data. Or it could be one of the Source streams.

export class ChecklistItemService {
  private storageService = inject(StorageService);

  private checklistItemsLoaded$ = this.storageService
    .loadChecklistItems()
    .pipe(toSource("[Storage] checklist items loaded"));

  add$ = new Source<AddChecklistItem>("[Checklist Items] add");
  remove$ = new Source<RemoveChecklistItem>("[Checklist Items] remove");
  edit$ = new Source<EditChecklistItem>("[Checklist Items] edit");
  checklistRemoved$ = new Source<RemoveChecklist>(
    "[Checklist Items] checklistRemoved$"
  );
  toggle$ = new Source<RemoveChecklistItem>("[Checklist Items] toggle");
  reset$ = new Source<RemoveChecklist>("[Checklist Items] reset");

A State Adapt “source” stream essentially behaves as a subject that we can next. By supplying extra strings, we can integrate with Redux devtools, making it easier to track data sources and state changes during debugging. We also pipe our normal observable stream with toSource to add this information as well.

The second part is our store. This gives us access to the current state. Here, we give the checklistItems state its initial state, and then supply an adapter. This adapter takes the data emitted on our data sources and determines how that should change the state. The idea is quite similar to actions and reducers in the Redux pattern.

  private store = adapt(["checklistItems", initialState, checklistItemsAdapter], {
    loadChecklistItems: this.checklistItemsLoaded$,
    add: this.add$,
    remove: this.remove$,
    edit: this.edit$,
    toggle: this.toggle$,
    reset: this.reset$,
    clearChecklistItems: this.checklistRemoved$,
  });

We hook up our data sources to the methods they should trigger in the adapter. When checklistItemsLoaded emits a value it should trigger loadChecklistItems, when the add source emits a new value it should trigger add in the adapter, and so on.

Then, in the adapter, just like with a reducer, we take in the data from the data source, along with our current state, and determine how that event should change the state. We can also define selectors in our adapter for the streams of data we want to create from our state.

code describing checklists

Utilizing State Adapt with Angular

Using the streams from those selectors, I convert them into signals that are publicly exposed on this service. I then use those signals in my components. At the end, all my component gets is a signal of the current state. The signals aspect here isn’t really a part of state adapt, it is just how I am using the state from state adapt in angular.

  private store = adapt(["checklistItems", initialState, checklistItemsAdapter], {
    loadChecklistItems: this.checklistItemsLoaded$,
    add: this.add$,
    remove: this.remove$,
    edit: this.edit$,
    toggle: this.toggle$,
    reset: this.reset$,
    clearChecklistItems: this.checklistRemoved$,
  });

  loaded = toSignal(this.store.loaded$, { requireSync: true });
  checklistItems = toSignal(this.store.checklistItems$, { requireSync: true });

To trigger changes like adding a new checklist item, the component simply nexts the appropriate data source from the service.

<app-checklist-item-list
    [checklistItem]="items()"
    (toggle)="cis.toggle$.next($event)"
    (delete)="cis.remove$.next($event)"
    (edit)="checklistItemBeingEdited.set($event)"
/>

A Comparison with Redux and NgRx Store

One might wonder how this is different from Redux and NgRx Store. We have what are basically actions being listened to by what are basically reducers to produce state that we can access with selector streams.

image of ngrx

The key difference here centres around the importance of our data sources as the source of change and not utilising callback functions that can contain imperative code.

A core philosophy of State Adapt is to avoid, and I think Mike would advocate for entirely eliminating, callback functions. Notice in the component that there are no methods. If we look at the other smart component in this application we will see it is the same deal.

item = computed(() =>
  this.cis
    .checklistItem()
    .filter((item) => item.checklistId === this.params()?.get("id"))
);

checklist = computed(() =>
  this.cs
    .checklist()
    .find((checklist) => checklist.id === this.params()?.get("id"))
);

  checklistItemForm = this.fb.nonNullable.group({
    title: ["", Validators.required],
  });

  constructor() {
    // TODO: Use [patchValue] directive to react to signal in template
    effect(() => {
      const checklist = this.checklistBeingEdited();

      if (item) {
        this.checklistItemForm.patchValue({
          title: item.title,
        });
      }
    });
  }
}

Whenever the user interacts with something in the template, rather than using a callback functions we have that action directly next a data source.

The only function left here is this one in the effect that patches the form value, and as we can see from the comment left by Mike this should ideally be eliminated too:

// TODO: Use [patchValue] directive to react to signal in template

I like how well this quote from one of Mike’s articles sums up the benefit of the anti-callback method philosophy:

“The curly braces of functions that don’t return anything are like open arms inviting imperative code.”

The Callback Method

Callback methods make it easy to drop in imperative code, directly nexting your data sources encourages you to be more declarative.

Progressive reactivity rule #2

For example, I was very clearly violating the declarative approach by making this imperative call in a callback:

remove(id: string) {
  this.checklistItemService.clearChecklistItems(id);
  this.remove$.next(id);
}

By having the remove method in the first place, it made it all too easy to sneak some imperative code in.

So, to make this more declarative, rather than having an imperative method to call clearChecklistItems as a side effect, the checklist being removed can just be a data source for both the checklist and checklistItem adapter. The checklist adapter will get the event from the data source and remove the checklist, and the checklistItem adapter will also get the event from the same data source and remove all of the checklist items associated with that checklist.

Removing callback method

If we want to delete a checklist, now we no longer require the callback method. We just call remove$.next with the event directly from the template.

<app-checklist-list [checklistItem]="checklists()" (delete)="cs.remove$.next($event)" (edit)="checklistBeingEdited.set($event)" />

Conclusion

I’m really enjoying using State Adapt so far and I think it’s likely that it will become the default state management library I reach for. The biggest problem it has right now is that it is not well known.

It’s a new library that was built and is currently being supported by Mike so of course it comes with that open source maintenance risk. But that’s the case with most new projects, and the way we get any of the projects we like off the ground is to support them however we can.

The 1.0 release of State Adapt has already been out for a while and at this point, Mike is mostly looking for help in the form of people using it and creating issues if they run into any problems.

Even if you don’t want to use State Adapt in production, trying it out is a fantastic exercise in learning to code more declaratively.

Learn to build modern Angular apps with my course