1
votes

I am beginning to get some proficiency in RxJS operators but I still have difficulty with some of them. In implementing a search component I have the following code:

      searchResult$: Observable<LunrDoc []>;
      searchResultLength$: Observable<number>;

      ngAfterViewInit(): void {

        // observable to produce an array of search hits

        this.searchResult$ = fromEvent<Event>(this.searchInput.nativeElement, 'keyup').pipe(
          debounceTime(1000),
          switchMap(e => this.ss.search((e.target as HTMLTextAreaElement).value)),
          share()
        );

        // observable to return the length of the array

        this.searchResultLength$ = this.searchResult$.pipe(
          map(sr => sr ? sr.length : 0),
          share()
        );
      }

And this is used in the template like this:

    <p *ngIf="(searchResultLength$ | async) > 0">
      Total Documents: {{ (searchResultLength$ | async) | number }}
    </p>

    <p *ngFor="let doc of (searchResult$ | async)">
      <span *ngIf="doc.path" [routerLink]="doc.path" style="color: darkblue; font-weight: bold; text-underline: darkblue; cursor: pointer">
        {{ doc.title }}
      </span>
      {{ doc.content.substring(0, 400) }}
    </p>

What happens is when a non-null array is emitted by the searchResult$ observable the rendered result in the first paragraph element as "Total Documents:" without a number after it. The *ngFor decorated paragraph works exactly as expected.

The reason I believe is that the second async pipe gets activated and subscribes to the shared observable after the last value gets emitted. So it never gets a "next" call.

Is there an RxJS operator to use instead of share to fix this situation? Or did I miss something else?

1
Your explanation sounds reasonable and you can try to use shareReplay(1) instead. - Ingo Bürk
I don't see a reason for share at all, do you really need it? - MoxxiManagarm
The share multicasts the observable such that the search API isn't hit for each subscription individually. The share on the length observable isn't necessary, though. The code could also use the "as" feature of ngIf to avoid a bit of repetition. - Ingo Bürk

1 Answers

1
votes

As @Ingo Bürk suggested. shareReplay(1) is probably what you are looking for. The 1 refers to the buffer size (how many values it should play back to you).

Share replay docs

What I'll also suggest is to avoid creating multiple observables.

e.g. This creates two different subscribers.

<p *ngIf="(searchResultLength$ | async) > 0">
  Total Documents: {{ (searchResultLength$ | async) | number }}
</p>

The alternative is:

<p *ngIf="(searchResultLength$ | async) as searchResultLength > 0">
  Total Documents: {{ searchResultLength | number }}
</p>

Proposed Solution

Looking at the code and trying to understand the functionality, I think you can make these changes to get your desired output.

Component:

searchResult$: Observable<LunrDoc []>;
    
ngAfterViewInit(): void {
    this.searchResult$ = fromEvent<Event>(this.searchInput.nativeElement, 'keyup')
    .pipe(
        debounceTime(1000),
        switchMap(e => this.ss.search((e.target as HTMLTextAreaElement).value)),
    );
}

You wouldn't need share or shareReplay as they don't seem to offer any functionality that your template will need. Unless there is some more code in the template that isn't visible in your question.

The this.searchResultLength$ observable also seems redundant as that is just the length property on the value returned by this.searchResult$.

The template:

<ng-container *ngIf="{searchResult: searchResult$ | async} as vm">
    <p *ngIf="vm.searchResult.length > 0">
        Total Documents: {{ vm.searchResult }}
    </p>

    <p *ngFor="let doc of vm.searchResult">
        <span *ngIf="doc.path" [routerLink]="doc.path" style="color: darkblue; font-weight: bold; text-underline: darkblue; cursor: pointer">
            {{ doc.title }}
        </span>
        {{ doc.content.substring(0, 400) }}
    </p>
</ng-container>

The wrapper ng-container will always display because the ngIf condition will evaluate to a truthy value.

Note

Consider looking into the ditinctUntilChanged operator, when dealing with inputs that trigger an observable: https://www.learnrxjs.io/operators/filtering/distinctuntilchanged.html