Issue
While refactoring my angular application I basically want to get rid of all subscriptions in order to use only async
pipe provided by angular (just a declarative approach instead of an imperative one).
I have problems to implement a declarative approach when multiple sources can lead to changes in the stream. If we only had one source then of course, I could just use scan
operator to build up my emitted values.
Scenario
Let's say I just want to have a simple component, where an array of strings is resolved during routing. In the component I want to display the list and want to be able to add or remove items using buttons.
Limitations
- I don't want to use
subscribe
, since I want angular to take care of unsubscription using (async
pipe) - I don't want to use BehaviorSubject.value, since it's (from my point of view) an imperative approach instead of a declarative one
- Actually I don't want to use any kind of subject at all (apart from the ones used for button
click
event propagation), since I don't think it is necessary. I should already have all needed observables, which just have to be "glued together".
Current process so far My journey so far took several steps. Please note that all approaches worked fine, but each has their individual downsights):
- Usage of
BehaviorSubject
and.value
to create the new array --> not declarative - Trying
scan
operator and create anAction
interface, where each button emits an action of typeXY
. This action would be read inside the function passed toscan
and then use a switch to determine which action to take. This felt a little bit like Redux, but it was a strange feeling to mix different value types in one pipe (first initial array, afterwards actions). - My so far favorite approach is the following: I basically mimic a BehaviorSubject by using
shareReplay
and use this instantly emitted value in my button, by switching to a new observable usingconcatMap
, where I only take 1 value in order to prevent creating a loop. Example implementation mentioned below:
list-view.component.html:
<ul>
<li *ngFor="let item of items$ | async; let i = index">
{{ item }} <button (click)="remove$.next(i)">remove</button>
</li>
</ul>
<button (click)="add$.next('test2')">add</button>
list-view.component.ts
// simple subject for propagating clicks to add button, string passed is the new entry in the array
add$ = new Subject<string>();
// simple subject for propagating clicks to remove button, number passed represents the index to be removed
remove$ = new Subject<number>();
// actual list to display
items$: Observable<string[]>;
constructor(private readonly _route: ActivatedRoute) {
// define observable emitting resolver data (initial data on component load)
// merging initial data, data on add and data on remove together and subscribe in order to bring data to Subject
this.items$ = merge(
this._route.data.pipe(map((items) => items[ITEMS_KEY])),
// define observable for adding items to the array
this.add$.pipe(
concatMap((added) =>
this.items$.pipe(
map((list) => [...list, added]),
take(1)
)
)
),
// define observable for removing items to the array
this.remove$.pipe(
concatMap((index) =>
this.items$.pipe(
map((list) => [...list.slice(0, index), ...list.slice(index + 1)]),
take(1)
)
)
)
).pipe(shareReplay(1));
}
Nevertheless I feel like this should be the easiest example possible and my implementation seems to complex for this kind of issue. It would be great if someone could help in finding a solution to this, what should be a simple, problem.
You can find a StackBlitz example of my implementation here: https://stackblitz.com/edit/angular-ivy-yj1efm?file=src/app/list-view/list-view.component.ts
Solution
You can create a modifications$
stream that takes each emission from your "modification subjects", and maps them to a function that will modify the state accordingly:
export class AppComponent {
add$ = new Subject<string>();
remove$ = new Subject<number>();
private modifications$ = merge(
this.add$.pipe(map(item => state => state.concat(item))),
this.remove$.pipe(map(index => state => state.filter((_, i) => i !== index))),
);
private routeData$ = this.route.data.pipe(map(items => items[ITEMS_KEY]));
items$ = this.routeData$.pipe(
switchMap(items => this.modifications$.pipe(
scan((state, fn) => fn(state), items),
startWith(items)
))
);
constructor(private route: ActivatedRoute) { }
}
Here we define items$
to start with the route data, then switch to a stream that applies the incoming reducer functions to the state. We use the initial items
from route data as our seed value inside scan
. We also use startWith
to initially emit the initial items
.
Here's a little StackBlitz sample.
Answered By - BizzyBob
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.