RxJS: Using CombineLatest() + StartWith() to remove code duplication

2018-03-26CODING ·angular rxjs

Using CombineLatest and StartWith (and DebounceTime) RxJS operators to remove code complexity and duplication when fetching paged search results05 June 2018 Update: Follow up post with further refactoring.
01 June 2018 Update: Updated code to RxJS6.
Scenario:
In my Angular5 application I have a search results page that contains three components:
Text-based filters component
Paged results list
Pagination component
After wiring up my code I ended up with something similar to below. This all worked fine, but as you can see it’s quite repetitive and not to mention hard to maintain.
results-list.component.html

results-list.component.ts

results$: Observable;
currentFilter: string = “Open”;
currentPage: number = 1;
readonly pageSize = 25;

onFilterUpdated(filter: string){
this.currentFilter = filter;
this.results$ = this.searchService.performSearch(this.currentPage,
filter, this.pageSize);
}

onPageUpdated(page: number){
this.currentPage = page;
this.results$ = this.this.searchService.performSearch(page,
this.currentFilter,this.pageSize);
}

ngOnInit(){
this.results$ = this.searchService.performSearch(this.currentPage,
this.currentFilter, this.pageSize);
}

Problem: Violation of DRY principle. Adding any extra form of triggering the search, will repeat the search line this.results$ = this.searchService.performSearch(this.currentPage, this.currentFilter);
Problem: If I want to subscribe to the results, I need to repeat the subscription each time I set this.results$. Don’t forget the associated unsubscription too.
Solution: Enter the combineLatest operator

When any observable emits a value, emit the latest value from each. https://www.learnrxjs.io

Next, we need the switchMap operator to finish off the combined observable and switch to the search observable

Map to observable, complete previous inner observable, emit values.https://www.learnrxjs.io

results-list.component.ts

results$: Observable;
@ViewChild(‘paginator’) paginator: PaginationComponent;
@ViewChild(‘filters’) filters: FilterComponent;
readonly pageSize = 25;

ngOnInit(){
const page$ = this.paginator.pageUpdated;
const filter$ = this.filter.filterUpdated;
this.results$ = combineLatest(page$, filter$, (p, f) = > {return {page: p, filter: f}}).pipe(
switchMap(r = > this.searchService.performSearch(r.page, r.filter, this.pageSize));
}

New Problem: This option doesn’t allow for an initial search on initialisation of the page.
Solution: enter the StartWith operator

Emit given value first.https://www.learnrxjs.io

results-list.component.ts

const page$ = this.paginator.pageUpdated.startWith(1);
const filter$ = this.filter.filterUpdated.startWith(”);

New Problem: Each binding on the UI to result$ resulted in a seperate call to the search service (even if we add a debounce in).
Solution: Subscribe to result and store results locally
Problem: Page number should reset to 1 when the filter updates
Solution: Store the current page, bind to it on the paginator and raise the event whenever the value changes. note: I’m still unsure if I’m 100% happy with this option
results-list.component.ts

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));

Bonus: Add a debounce to minimise extra calls to the service
Final code
results-list.component.html

Read More