Refactoring a Complex State Service
I mentioned at the end of the last lesson that the first example we looked at
for implementing connect
was a bit of an ideal case that ends up looking
super clean. It is still realistic, and often our state will end up looking
like this (in fact, the example we looked at was a snippet from the next
application we will be building).
However, things are complicated a little when we also need to access the previous state in order to set our new state. Our Quicklists application does that a lot, so to see how this works we will refactor the services in the Quicklists application.
Again, if you do not already have the completed Quicklists application on your computer you can find it here.
IMPORTANT: Make sure that you have installed ngxtension
in the
application:
ng add ngxtension
Refactoring the Checklist Service
Let’s start by refactoring the ChecklistService
. The only thing we typically
need to touch when doing these refactors to use connect
is our reducers
— our sources and everything else can remain the same.
This is what the reducers look like for our ChecklistService
without
connect
:
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 })),
});
this.add$.pipe(takeUntilDestroyed()).subscribe((checklist) =>
this.state.update((state) => ({
...state,
checklists: [...state.checklists, this.addIdToChecklist(checklist)],
}))
);
this.remove$.pipe(takeUntilDestroyed()).subscribe((id) =>
this.state.update((state) => ({
...state,
checklists: state.checklists.filter((checklist) => checklist.id !== id),
}))
);
this.edit$.pipe(takeUntilDestroyed()).subscribe((update) =>
this.state.update((state) => ({
...state,
checklists: state.checklists.map((checklist) =>
checklist.id === update.id
? { ...checklist, title: update.data.title }
: checklist
),
}))
);
// effects
effect(() => {
if (this.loaded()) {
this.storageService.saveChecklists(this.checklists());
}
});
}
We will be refactoring all of this, but the bit specifically that we are focusing on is stuff like this:
this.add$.pipe(takeUntilDestroyed()).subscribe((checklist) =>
this.state.update((state) => ({
...state,
checklists: [...state.checklists, this.addIdToChecklist(checklist)],
}))
);
This source emits the new checklist
to add. However, in order to set the new
checklists
state we first need to access all of the existing checklists in the
previous state with ...state.checklists
and then add our new checklist to
those.
This is the way we used connect
in the last lesson:
constructor() {
// reducers
const nextState$ = merge(
this.userAuthenticated$.pipe(map(() => ({ status: 'success' as const }))),
this.login$.pipe(map(() => ({ status: 'authenticating' as const }))),
this.error$.pipe(map(() => ({ status: 'error' as const })))
);
connect(this.state).with(nextState$);
}
There is no previous state here — we just map our sources to whatever we want the next state to be.
However, and again you may remember this from the lesson where we broke down
exactly how connect
works, we can also optionally supply a reducer
function to connect
.
We will update the entire ChecklistService
in just a moment, but dealing with
the add$
source in isolation would look like this:
connect(this.state)
.with(this.add$, (state, checklist) => ({
checklists: [...state.checklists, this.addIdToChecklist(checklist)],
}))
Instead of just supplying the source
, we also supply a reducer function.
This reducer function will be given the previous state (state
in this example)
and whatever our source
emits (checklist
) in this example. We then just have
our reducer return whatever it is we want to set in the state signal — in this
case, we want to overwrite the checklists
property with our new checklists
array.