RxJS refactor: BehaviourSubjects

CODING ·
angular rxjs behavioursubject

Using RxJS BehaviourSubjects to tidy up our code and remove tight coupling between the Template and the component, whilst also providing hooks to trigger data updates.

In an earlier post I described how to leverage the power of RxJS and the combineLatest() and startWith() operators to tidy up my code. I have since updated the code on that page to reflect the new RxJS6 pipe operators.

I was never really 100% happy with this solution for two reasons in particular: I still had to subscribe to the results (this was in part resolved with my follow-up post, and I had to reference the ViewChild elements in the component. Although Angular allows us to reference the view elements using the ViewChild() operator, this isn’t the cleanest solution. 1) It tightly couples the view template to the implementation and logic of the component and 2) When looking at the template, the event bindings aren’t obvious.

Enter BehaviourSubjects.

It stores the latest value emitted to its consumers, and whenever a new Observer subscribes, it will immediately receive the “current value” from the BehaviorSubject.source

Kind of sounds like what we are trying to achieve with our searching and pagination. As a bonus, a BehaviourSubject takes an initial value. This now means that we can do away with our StartWith operator.

The original code currently looks like this

results-list.component.html
<app-filters></app-filters>
<app-results [items]="items"></app-results>
<app-paginator [totalPages]="totalPages"></app-paginator>

results-list.component.ts
items: result[];
totalPages: number;
readonly pageSize = 25;
@ViewChild('paginator') paginator: PaginationComponent;
@ViewChild('filters') filters: FilterComponent;

ngOnInit(){
   const page$ = this.paginator.pageUpdated.pipe(
      startWith(1),
      tap(x=> this.currentPage = 1));
   const filter$ = this.filter.filterUpdated.pipe(
      startWith(''),
      tap(x => this.currentPage = x));

    combineLatest(page$,filter$, (p,f) => { return {page: p, filter: f}}).pipe(
        debounceTime(200),
        switchMap(r => this.searchService.performSearch(r.page, r.filter)))
       .subscribe(results => {
                               this.items = results.items;
                               this.totalpages = results.totalItems / this.pageSize;});

   }

Final code

results-list.component.html
<app-filters (filter)="filter$.next($event); page$.next(1)"></app-filters>
    <div *ngIf="results$ | async as results; else loading">
    <app-results [items]="results.items"></app-results>
    <app-paginator [totalPages]="results.totalItems / pageSize" (pageUpdated)="page$.next($event)"></app-paginator>

results-list.component.ts
   results$: Observable<SearchResults>;
   currentPage$: BehaviourSubject(1);
   filter$: BehaviourSubject(null);

   readonly pageSize = 25;

   ngOnInit(){
    items$ = combineLatest(page$,filter$, (p,f) => { return {page: p, filter: f}}).pipe(
        debounceTime(300),
        switchMap(r => this.searchService.performSearch(r.page, r.filter))
        );
   }

Another benefit this optimisation gives us is the ability to modify our filters and pagination from any source, not just from the components raising the original events.

results-list.component.ts
results$: Observable<SearchResults>;
currentPage$: BehaviourSubject(1);
filter$: BehaviourSubject(null);
readonly pageSize = 25;

ngOnInit(){
    items$ = combineLatest(page$,filter$, (p,f) => { return {page: p, filter: f}}).pipe(
        debounceTime(300),
        switchMap(r => this.searchService.performSearch(r.page, r.filter))
        );
   }

clearResults(){
       this.currentPage$.next(1);
       this.filter$.next('');
   }

Wow… From the original mess and duplicated code to this. Reactive programming and RxJS may have a steep learning curve, but once mastered can really tidy up your event based code. Thanks Ben and the RxJS team