RxJS refactor: BehaviourSubjects
CODING ·
angular rxjs behavioursubject
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