3d cartoon hands holding a phone

Unlock full course by purchasing a membership

Lesson 14

Resource API Refactor

STANDARD

Resource API Refactor

As I mentioned in the introductory modules, the resource API offers a lot of power, and a great developer experience, but it also sort of muddies the boundary between the role of RxJS and signals.

Since these APIs are still marked as experimental in Angular I did not want to rely on using them as the default approach, but it likely will become the default at some point.

In this lesson, we are going to look at how to refactor the application to utilise these APIs.

Refactoring the Storage Service

The first thing we are going to do is refactor the load methods in our storage service.

Update the load methods in the StorageService:

  loadChecklists() {
    return resource({
      loader: () =>
        Promise.resolve(this.storage.getItem('checklists')).then(
          (checklists) =>
            checklists ? (JSON.parse(checklists) as Checklist[]) : [],
        ),
    });
  }

  loadChecklistItems() {
    return resource({
      loader: () =>
        Promise.resolve(this.storage.getItem('checklistItems')).then(
          (checklistItems) =>
            checklistItems
              ? (JSON.parse(checklistItems) as ChecklistItem[])
              : [],
        ),
    });
  }

An important point to note here is that we are wrapping this.storage.getItem in Promise.resolve. This is really just for learning purposes, because our storage is actually synchronous. Data can be retrieved immediately, there is no need for a “load” and no need for resource.

But, usually when loading data (e.g. from some external API) it is going to be asynchronous. So, by wrapping our synchronous method in Promise.resolve we can turn it into an asynchronous method. But do keep in mind that this is just so we can play around with resource. In a normal application, it would just be complicating things for no reason to wrap something synchronous like this in a Promise.

We are using the most basic implementation of resource here. We set up our loader and pass it the Promise we want to use to load data. If this promise returns valid items we JSON.parse and return those items, otherwise we just return an empty array.

Refactoring the ChecklistService

Now we are going to modify ChecklistService to utilise resource and linkedSignal. We are going to use resource (via the implementation we already have in the StorageService) to handle loading our checklist data.

We will then use linkedSignal to take that data and create a WritableSignal that we can then update with our action sources (e.g. add$, edit$).

It is a big ask, but if you’re up to the challenge see if you make some progress toward getting this implemented by yourself before continuing. Even just thinking about some of the things that might be changed will be helpful.

Modify the the sources, state, and constructor in ChecklistService:

import {
  Injectable,
  ResourceStatus,
  effect,
  inject,
  linkedSignal,
} from '@angular/core';
  // sources
  loadedChecklists = this.storageService.loadChecklists();
  add$ = new Subject<AddChecklist>();
  edit$ = new Subject<EditChecklist>();
  remove$ = this.checklistItemService.checklistRemoved$;

  // state
  checklists = linkedSignal({
    source: this.loadedChecklists.value,
    computation: (checklists) => checklists ?? [],
  });

  constructor() {
    this.add$
      .pipe(takeUntilDestroyed())
      .subscribe((checklist) =>
        this.checklists.update((checklists) => [
          ...checklists,
          this.addIdToChecklist(checklist),
        ]),
      );

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

    this.edit$
      .pipe(takeUntilDestroyed())
      .subscribe((update) =>
        this.checklists.update((checklists) =>
          checklists.map((checklist) =>
            checklist.id === update.id
              ? { ...checklist, title: update.data.title }
              : checklist,
          ),
        ),
      );

    // effects
    effect(() => {
      const checklists = this.checklists();
      if (this.loadedChecklists.status() === ResourceStatus.Resolved) {
        this.storageService.saveChecklists(checklists);
      }
    });
  }

There are a few differences here, so let’s talk through them.

The biggest difference is that we have replaced our checklistsLoaded$ source with loadedChecklists which contains the result from the resource API. This means that we no longer have the subscribe to checklistsLoaded$ in the constructor and we don’t need to worry about manually handling the loaded and error state so we are able to remove those as well.

This is a pretty big win, this is all of the boilerplate we are able to just instantly get rid of:

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

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

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

this.checklistsLoaded$.pipe(takeUntilDestroyed()).subscribe({
  next: (checklists) =>
    this.state.update((state) => ({
      ...state,
      checklists,
      loaded: true,
    })),
  error: (err) => this.state.update((state) => ({ ...state, error: err })),
});
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).