As I refactor my Angular application, my goal is to eliminate all subscriptions and rely solely on the async
pipe provided by Angular for a declarative approach instead of an imperative one.
I encounter difficulties implementing a declarative approach when multiple sources can trigger changes in the stream. If there was only one source, I could simply use the scan
operator to accumulate my emitted values.
Scenario:
Imagine a simple component where an array of strings is resolved during routing. In this component, I aim to display the list and allow users to add or remove items using buttons.
Constraints:
- Avoiding the use of
subscribe
as I want Angular to handle unsubscription through theasync
pipe. - Steering clear of BehaviorSubject.value as it feels more like an imperative approach rather than a declarative one.
- Avoiding any form of subject usage except for button click event propagation, believing that the necessary observables should be in place and just need to be connected together.
Current Progress:
My journey so far has gone through several stages, each with its own challenges:
- Using
BehaviorSubject
and.value
to generate the new array – not truly declarative. - Experimenting with the
scan
operator and creating anAction
interface where each button emits an action type. This felt somewhat reminiscent of Redux but mixing different value types within one pipe felt awkward. - My preferred approach thus far involves simulating a BehaviorSubject using
shareReplay
and instantly emitting the value into the button by transitioning to a new observable usingconcatMap
, limiting it to 1 value to prevent creating a loop.
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
// Define simple subjects for adding and removing items
add$ = new Subject<string>();
remove$ = new Subject<number>();
// Observable holding the actual displayed list
items$: Observable<string[]>;
constructor(private readonly _route: ActivatedRoute) {
// Define observable emitting resolver data
// Merge initial data, data on addition, and removal to bring the data to Subject
this.items$ = merge(
this._route.data.pipe(map(items => items[ITEMS_KEY])),
// Observable for adding items to the array
this.add$.pipe(
concatMap(added =>
this.items$.pipe(
map(list => [...list, added]),
take(1)
)
)
),
// Observable for removing items from 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));
}
Despite being what seems like a straightforward example, I find my implementation overly complex for such a task. Any guidance towards simplifying this process would be greatly appreciated.
You can access a StackBlitz demonstration of my implementation here: StackBlitz Demo