Reputation: 1404
I am rewriting some code to use RXJS and the angular async pipe. The code is rather simple we are going to add and remove items from an array. Is there a better way to accomplish manipulating a shared array than using tap set store the state in a BehaviorSubject?
this.listSubject = new BehaviorSubject(['a','b','c']);
this.addSubject = new Subject<string>();
this.addAction$ = this.addSubject.asObservable().pipe(
withLatestForm(this.listSubject),
map(([itemToAdd, list]) => {list.push(itemToAdd); return list;}),
tap((data) => this.listSubject.next(data))
);
this.removeSubject = new Subject<number>();
this.removeAction$ = this.removeSubject.asObservable().pipe(
withLatestForm(this.listSubject),
map(([indexToRemove, list]) => {list.splice(indexToRemove, 1); return list;}),
tap((data) => this.listSubject.next(data))
);
this.list$ = merge(this.addAction$, this.removeAction$);
EDIT: The UI code is using async pipe,
list$ | async
Upvotes: 0
Views: 548
Reputation: 60518
Something like this works:
export class HelloComponent {
actionSubject = new Subject<Action>();
action$ = this.actionSubject.asObservable();
originalList$ = of(["a", "b", "c"]);
list$ = merge(this.originalList$, this.action$).pipe(
scan((acc: string[], action: any) => {
if (action.isDelete) {
return acc.filter(item => item !== action.item);
} else {
return [...acc, action.item];
}
})
);
onAdd() {
this.actionSubject.next({ item: "z", isDelete: false });
}
onRemove() {
this.actionSubject.next({ item: "b", isDelete: true });
}
}
export interface Action {
item: string;
// This could instead be an Enum of operations
isDelete: Boolean;
}
NOTE: I did a stackblitz of your code here: https://stackblitz.com/edit/angular-array-processing-deborahk
I created a single action stream that emitted an action object with the item to add/remove and whether the action is a delete or not. (You could change this to an enum for add and delete)
I then used scan
to retain the set of items over time and simply add to or remove from that list of items.
Your code is in the app.component.ts
file
The revised code is in the hello.component.ts
file.
Does that work for your scenario?
NOTE: To those that said you need to subscribe ... you don't if you use the async pipe. My UI looks like this:
<div>{{ list$ | async}}</div>
Upvotes: 3
Reputation: 21628
Why would you not use functions?
list$ = new BehaviorSubject(['a','b','c']);
add(val) {
this.list$.next([...this.list$.value, val]);
}
remove(val) {
this.list$.next(...this.list$.value.filter(v => v !== val));
}
Your code is not clear, what is subscribing to all these observables. You would need subscriptions to your action observables to trigger any functionality. You are mutating objects.
Upvotes: 1
Reputation: 43797
Generally speaking most usage of tap
will concern RxJS hard liners.
The biggest concern I have is that you really should be modifying your application state (the behavior subject) with a subscription and not a tap. You currently don't show any subscribers to list$
. If no one ever subscribes then your tap
will never run. On the flip side, if there end up being two subscribers to list$
then your taps will run twice for each event! You could mitigate this with a publish
somewhere but I think it would be a lot cleaner if you just had...
this.listSubject = new BehaviorSubject(['a','b','c']);
this.addSubject = new Subject<string>();
this.addSubject.subscribe(itemToAdd => {
const currentValue = this.listSubject.value;
currentValue.push(itemToAdd);
this.listSubject.next(currentValue);
});
this.removeSubject = new Subject<number>();
this.removeSubject.subscribe(indexToRemove => {
const currentValue = this.listSubject.value;
currentValue.splice(indexToRemove, 1);
this.listSubject.next(currentValue);
});
The second (slightly lesser) concern I have is that you are re-emitting your list from multiple subjects. You now have three subjects, each of which can emit the same list. This means you have three potential sources of truth. You're also violating CQS (command query separation). When something calls addItem
it shouldn't need to grab the new list value. Instead, whatever is consuming the list will simply get the update because your list subject is updated.
Extra Reading: In your app here you have state (the list) and events that can modify state. Or, more generally, "actions" that can modify your state. In a tool like ngrx
you will see concepts like "stores" (your state) and "actions" (your events) to describe this model. Behavior subjects are a lightweight alternative to something like ngrx
store. As your app starts to get more and more complex you may gain quite a bit from reading up on tools like ngrx store.
Upvotes: 2
Reputation: 3218
Looks really nice your code, the only thing I could think to add would be immutability. That way you'd get a new list every time you add or remove items to it:
this.addAction$ = this.addSubject.asObservable().pipe(
withLatestForm(this.listSubject),
map(([itemToAdd, list]) => [...list, itemToAdd]),
tap((data) => this.listSubject.next(data))
);
this.removeAction$ = this.removeSubject.asObservable().pipe(
withLatestForm(this.listSubject),
map(([indexToRemove, list]) =>
[...list.slice(0, indexToRemove), ...list.slice(indexToRemove + 1)]),
tap((data) => this.listSubject.next(data))
);
Upvotes: 0