Fynn
Fynn

Reputation: 4883

Updating a value in an observable without resolving prior operators

Here is what I am trying to do: I have a list of users coming from an async data source. The source returns an Observable<User[]>. The list is then displayed in the template via Angular's async pipe like this.

<ng-template #loading>Loading...</ng-template>
<ul *ngIf="users$ | async; let users; else loading">
  <li *ngFor="let user of users">{{user.name}}</li>
</ul>

The list of users is searchable. Using reactive forms, I have bound the search field's change event like this (with this.resolve performing the async task, having the signature (text: string) => Observable<User[]>):

search = new FormControl('');
users$ = this.search.valueChanges
  .pipe(startWith(this.search.value))
  .pipe(debounceTime(250))
  .pipe(distinctUntilChanged())
  .pipe(switchMap(this.resolve));

This works perfectly fine. The search input is debounced and a new async request to resolve the users is fired when the search input changes.

Now I have a problem. Users may change on the client side and need to be updated in the list without performing the async task again. My attempt to use another pipe operation in the update function results in this.resolve being executed again.

update(user: User): void {
  this.users$ = this.users$
    .pipe(map(users => {
      // replace user in users
      return users;
    }));
}

However, I do not want to resolve the users again. I just want to locally update the async user list. How can I do this?

Upvotes: 0

Views: 1158

Answers (2)

Jeff Fairley
Jeff Fairley

Reputation: 8314

Adding on to martin's merge() suggestion, I would also "share" the users result to prevent the resolve from firing again.

RXJS will run through all the pipes every time a subscription is added. Think of them as transformers where the "datastore" is the initial Subject (this.search.valueChanges). So every time you subscribe, RXJS is going to run through your pipes to get the transformed value.

So you want to look at RXJS share() or shareReplay(1). What these will do is internally create a new Subject() (in the case of share()) or a new ReplaySubject(1) (in the case of shareReplay(1)) to hold your transformed value in a new "datastore", if you will.

search = new FormControl('');
otherUsers$ = new Subject();
users$ = merge(
  otherUsers$,
  this.search.valueChanges.pipe(
    startWith(this.search.value)),
    debounceTime(250)),
    distinctUntilChanged()),
    switchMap(this.resolve))
  )
).pipe(share());

Subject vs ReplaySubject depends on whether you want to hold a value even when there is no subscription. (This is my interpretation based on my own usage.) I found shareReplay(1) useful in cases where I have subscriptions like obs.pipe(first()).subscribe(...). However one of your subscriptions is the AsyncPipe in the DOM, so you're probably fine with share(), but try it out yourself.

Upvotes: 1

martin
martin

Reputation: 96909

You can use Subject and merge it into the users$ chain after switchMap.

otherUsers$ = new Subject();

users$ = this.search.valueChanges
  .pipe(
    startWith(this.search.value),
    debounceTime(250)),
    distinctUntilChanged()),
    switchMap(this.resolve),
    merge(otherUsers$),
  );

Then you can call next() and avoid going through the whole chain:

otherUsers$.next([{}, {}, {}, ...]);

Upvotes: 3

Related Questions