Search code examples
angularrxjscombinelatest

Why does CombineLatest modify observable


In my Angular app I have this simple stream for a search bar.

.ts

user$: Observable<User> = this.authService.user$;
ally$: Observable<UserAndMatch> = this.user$.pipe(
  switchMap((user: User) =>
    this.userService.getFollowing(user)
  shareReplay(1),
);
filteredAlly$: Observable<UserAndMatch> = new Observable<UserAndMatch>();
@ViewChild('search') search: ElementRef;

ngAfterViewInit() {
  const query$ = fromEvent(this.search.nativeElement, 'input').pipe(
    debounceTime(200),
    map(() => this.search.nativeElement.value),
    startWith(''),
    map((query: string) => query.trim().toLowerCase()),
    distinctUntilChanged(),
  );

  this.filteredAlly$ = combineLatest([query$, this.ally$]).pipe(
    map(([query, ally]: [string, UserAndMatch]) => {
      return !query ? ally : this.allyFilter(ally, query)
    }),
  );
}

 allyFilter(ally, query) {
   ally.users = ally.users.filter(user => user.name.includes(query));
   return ally;
 }

.html

  <form autocomplete="off">       
       <input
         #search
       />
   </form>
   <ng-container *ngrxLet="filteredAlly$ as ally"></ng-container

The stream works as expected when typing a word, but when the user deletes some letters the array from filteredAlly$ doesn't repopulate and, logging the values in the ally$ pipeline, I found out that ally$ values are being filtered too, while I wanted them to be a "backup" to apply some filter function to which should return filteredAlly$


Solution

  • The probable culprit is the first line of allyFilter:

    ally.users = ally.users.filter(user => user.name.includes(query));
    

    You're modifying ally.users in place, instead of creating a copy. This means that you're shortening the list of allies when the user is typing, and you're not getting a "fresh" copy of allies later, when the user deletes the characters.

    It will probably suffice to shallow-copy ally object in allyFilter, like so:

     allyFilter(ally, query) {
       return {
         ...ally,
         users: ally.users.filter(user => user.name.includes(query));
       };
     }