Skip to content

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.

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.

import {
Injectable,
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() === '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 })),
});

Instead of all of that we just have:

loadedChecklists = this.storageService.loadChecklists();

Pretty cool, right? But we’ve also added this:

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

This takes the value that is loaded in and will return the checklists if there are any, or otherwise it will just be an empty array.

This is because we still need to be able to update the checklists after they have loaded. The linkedSignal allows us to take the value from loadedChecklists, run some computation to create this new value, but it then also still allows us to update the value.

You will notice that is exactly what we are doing with the rest of our sources in the constructor. We subscribe to the source just as before, and update the checklists linkedSignal in response.

Another interesting change here is this:

effect(() => {
const checklists = this.checklists();
if (this.loadedChecklists.status() === 'resolved') {
this.storageService.saveChecklists(checklists);
}
});

We only want to trigger our save after the initial set of checklists have loaded in (and every time they change after that).

With the values returned from resource we are able to easily check the status of our load, and we can check if the load has Resolved before triggering a save.

Why not use signals instead of RxJS?

Perhaps you might be wondering why we even still need RxJS here for add/edit/remove. Why not have signals handle that as well?

For example, rather than this process:

  1. Have an add$ Subject
  2. Call .next on that Subject with the action
  3. Subscribe to add$ in the constructor to update a signal

Why not something like this:

  1. Have an add Signal
  2. Call .set on that Signal with the action
  3. Have an effect in the constructor to update a signal when add changes

On the surface, this process seems to work just fine and completely removes any dependency we have on RxJS.

But an important distinction to keep in mind is that signals are synchronous and not really designed nor suited to handling events which are by nature asynchronous (Angular’s resource implementation is sort of a special case in this regard).

There are a couple of issues here. One issue is that our effect would only run if the value we set on the add signal is actually different. This isn’t that big of a deal though, because if we are working with objects (as we are in this application) then objects are always considered to be different even if they contain the same data. But even if we wanted to trigger an action with some primitive value like 2 multiple times in a row, it is possible to set up a custom equality function for signals:

signal<undefined | number>(undefined, { equal: () => false });

The equality function we are providing here is always false, and so even if we set a signal with the same primitive values twice in a row, the signal will still consider those values to be different.

So, a bit of extra configuration, but not too bad. The real killer is how an effect is scheduled in Angular.

We would want the effect to run every time we call .set on the signal. But, an effect does not necessarily run every time a signal changes. There is timing/scheduling involved in settling the values of signals before running effects, and if we try to call set twice within the same “cycle” it won’t work.

If we were to call .set twice in a row, e.g:

checklistService.add.set({title: 'hi'});
checklistService.add.set({title: 'there'});

The effect will only run for the second .set. In the Quicklists application this isn’t actually a problem, because we don’t use actions in this way. We would just be calling set once in response to some user action and everything will work out just fine.

But that’s only by chance really, in my opinion it’s a very fragile mechanism prone to gotchas. We should be able to rely on the fact that if we call any of our actions they will actually be triggered.

This is what RxJS is able to give us. There will be no missed actions/events with RxJS.

Using the Refactored ChecklistService

We have only made very slight changes to the API our service exposes, so we are only going to need to make minor changes elsewhere. The primary difference is that we just have our checklists signal now for accessing the checklists data. If we need to react to the loading or error states, those are available via loadedChecklists.status() and loadedChecklists.error().

I will leave it to you to update the HomeComponent appropriately. You should see type errors for anything that needs to be updated. If you get stuck, remember that the source code is available.

Refactoring the ChecklistItemService

The updates for the ChecklistItemService are almost identical to the ChecklistService. We replace the checklistItemsLoaded$ source, and all the boilerplate that goes along with it, with loadedChecklistItems that uses the resource API from our StorageService.

We will need to add the linkedSignal for checklistItems and we will need to update the constructor for each of our sources so that it is updating the linkedSignal appropriately. We also need to update the save effect.

See how much of this you can get done, basing it on the work we have already done with ChecklistService and then I will paste the solution below:

@Injectable({
providedIn: 'root',
})
export class ChecklistItemService {
private storageService = inject(StorageService);
// sources
loadedChecklistItems = this.storageService.loadChecklistItems();
add$ = new Subject<AddChecklistItem>();
remove$ = new Subject<RemoveChecklistItem>();
edit$ = new Subject<EditChecklistItem>();
toggle$ = new Subject<RemoveChecklistItem>();
reset$ = new Subject<RemoveChecklist>();
checklistRemoved$ = new Subject<RemoveChecklist>();
// state
checklistItems = linkedSignal({
source: this.loadedChecklistItems.value,
computation: (checklistItems) => checklistItems ?? [],
});
constructor() {
this.add$.pipe(takeUntilDestroyed()).subscribe((checklistItem) =>
this.checklistItems.update((checklistItems) => [
...checklistItems,
{
...checklistItem.item,
id: Date.now().toString(),
checklistId: checklistItem.checklistId,
checked: false,
},
]),
);
this.edit$
.pipe(takeUntilDestroyed())
.subscribe((update) =>
this.checklistItems.update((checklistItems) =>
checklistItems.map((item) =>
item.id === update.id
? { ...item, title: update.data.title }
: item,
),
),
);
this.remove$
.pipe(takeUntilDestroyed())
.subscribe((id) =>
this.checklistItems.update((checklistItems) =>
checklistItems.filter((item) => item.id !== id),
),
);
this.toggle$
.pipe(takeUntilDestroyed())
.subscribe((checklistItemId) =>
this.checklistItems.update((checklistItems) =>
checklistItems.map((item) =>
item.id === checklistItemId
? { ...item, checked: !item.checked }
: item,
),
),
);
this.reset$
.pipe(takeUntilDestroyed())
.subscribe((checklistId) =>
this.checklistItems.update((checklistItems) =>
checklistItems.map((item) =>
item.checklistId === checklistId
? { ...item, checked: false }
: item,
),
),
);
this.checklistRemoved$
.pipe(takeUntilDestroyed())
.subscribe((checklistId) =>
this.checklistItems.update((checklistItems) =>
checklistItems.filter((item) => item.checklistId !== checklistId),
),
);
// effects
effect(() => {
const checklistItems = this.checklistItems();
if (this.loadedChecklistItems.status() === 'resolved') {
this.storageService.saveChecklistItems(checklistItems);
}
});
}
}

Once again, you will also need to update components that are consuming this service.

Summary

We’ve made some pretty light changes here for some huge wins with the resource API, and I think that highlights just how useful it is. The next application we build will get into some more advanced use cases of both resource and linkedSignal.