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.
Click here to reveal solution
Solution
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 })),
});