Calidus
Calidus

Reputation: 1404

Shared Data between multiple RXJS Observables

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

Answers (4)

DeborahK
DeborahK

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

Adrian Brand
Adrian Brand

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

Pace
Pace

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

Fredrik_Borgstrom
Fredrik_Borgstrom

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

Related Questions