Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Angular Belgrade 2023 Infinite Scrolling Implem...

Angular Belgrade 2023 Infinite Scrolling Implemented using RxJS

Slides from my talk at Angular Belgrade 2023. The talk was named: Infinite Scrolling Implemented using RxJS.

The example project can be found here: https://github.com/jakovljevic-mladen/ng-pagination-rx-streams

Avatar for Mladen Jakovljević

Mladen Jakovljević

November 08, 2023
Tweet

Other Decks in Programming

Transcript

  1. Mladen Jakovljević Frontend developer @ goUrban RxJS Core Team member

    Twitter: twitter.com/@jakovljevicMla GitHub: github.com/jakovljevic-mladen
  2. What is RxJS? What is Observable? • JS object •

    From 0 to infinite values • Push based • Cancellable • Lazy
  3. Observables require subscription articles$.subscribe( articles => display(articles) ); <p>{{ articles$

    | async }}</p> articles$.subscribe({ next: articles => display(articles), error: error => displayError(error), complete: () => console.log('Completed') });
  4. Observables have an option to be cancelled const subscription =

    articles$. subscribe( articles => display(articles) ); // later subscription.unsubscribe(); // or articles$.pipe( takeUntil(this.destroy$) ).subscribe(...);
  5. Observables bring values and/or events (push based) • From 0

    to infinite many values (next) • A notification that there are no more values (complete) • A notification that an error happened during production of the values (error)
  6. As collections, we can transform them by - using operators

    • Filtering operators ◦ filter, take, takeUntil, first, last, ... • Math operators ◦ count, max, min, reduce • Transform operators ◦ map, scan, buffer, ... ◦ mergeMap, switchMap, exhaustMap, concatMap // higher order mapping
  7. How to make an Observable? const getData$ = of('Data?'); const

    arr = [1, 2, 3, 4]; const getMoreData$ = from(arr); const getDataFromAPI$ = from( fetch('https://api.github.com/search/users?q=mladen' ).then(res => res.json()) ); const tickEverySecond$ = interval(1_000); import { of, from, interval, fromEvent } from 'rxjs'; const onDocumentClick$ = fromEvent(document, 'click'); new Observable(...);
  8. Scroll events have issues • They happen too many times

    ◦ buffer, window, audit • We need to provide a threshold • They don't solve an issue with large amount of DOM elements in an easy way ◦ Threshold needs to be adjusted after each data load and we can never be sure that we did it the right way • What is the solution? ◦ IntersectionObserver • RxJS doesn't have out-of-the-box solution to create an IntersectionObserver Observable
  9. How to create an Observable from IntersectionObserver? function fromIntersectionObserver (

    target: Element, options?: IntersectionObserverInit ): Observable<IntersectionObserverEntry> { return new Observable(subscriber => { const callback = ([entry]: IntersectionObserverEntry[]) => { subscriber. next(entry); }; const io = new IntersectionObserver (callback, options); io.observe(target); return () => io.disconnect(); }); }
  10. import { Injectable } from '@angular/core'; import { HttpClient }

    from '@angular/common/http' ; import { catchError, EMPTY, Observable } from 'rxjs'; import { FakeFeedResponse } from '../models'; @Injectable({ providedIn: 'root' }) export class FeedService { private nextPage: number | null = 1; feed$: Observable<FakeFeedResponse> = this.http.get<FakeFeedResponse>( '/feed', { params: { nextPage: this.nextPage } }).pipe( catchError(() => EMPTY) ); constructor(private http: HttpClient) { } }
  11. class FeedService { private nextPage: number | null = 1;

    feed$: Observable<FakeFeedResponse> = this.http.get<FakeFeedResponse>( '/feed', { params: { nextPage: this.nextPage } }).pipe( catchError(() => EMPTY) ); constructor(private http: HttpClient) { } } interface FakeFeedResponse { page: number; nextPage: number | null; items: FeedItem[]; } interface FeedItem { id: string; user: { name: string, avatar: string }; type: 'text' | 'image'; text?: string; imageURL?: string; }
  12. class FeedComponent { feed$: Observable<FeedItem[]> = this.feedService.feed$.pipe( map(({ items })

    => items) ); constructor(private feedService: FeedService) { } }
  13. <div class="articles"> <article #article [id]="item.id" *ngFor="let item of (feed$ |

    async)"> <div class="user"> <img [src]="item.user.avatar" [alt]="item.user.name + ' avatar'"> {{item.user.name}} </div> <p *ngIf="item.type === 'text'">{{item.text}}</p> <img *ngIf="item.type === 'image'" [src]="item.imageURL" [alt]="item.imageURL"> </article> </div>
  14. class FeedService { private nextPage: number | null = 1;

    feed$: Observable<FakeFeedResponse> = this.http.get<FakeFeedResponse>( '/feed', { params: { nextPage: this.nextPage } }).pipe( catchError(() => EMPTY) ); constructor(private http: HttpClient) { } }
  15. class FeedService { private nextPage: number | null = 1;

    feed$: Observable<FakeFeedResponse> = this.http.get<FakeFeedResponse>('/feed', { params: { nextPage: this.nextPage } }).pipe( catchError(() => EMPTY) ); constructor(private http: HttpClient) { } }
  16. class FeedService { private nextPage: number | null = 1;

    feed$: Observable<FakeFeedResponse> = this.http.get<FakeFeedResponse>('/feed', { params: { nextPage: this.nextPage } }).pipe( catchError(() => EMPTY) ); constructor(private http: HttpClient) { } }
  17. private nextPage: number | null = 1; feed$: Observable<FakeFeedResponse> =

    this.http.get<FakeFeedResponse>('/feed', { params: { nextPage: this.nextPage } }).pipe( catchError(() => EMPTY) );
  18. private nextPage: number | null = 1; feed$: Observable<FakeFeedResponse> =

    this.http.get<FakeFeedResponse> ('/feed', { params: { nextPage: this.nextPage } }).pipe( catchError(() => EMPTY) );
  19. private nextPage: number | null = 1; feed$ = this.http.get('/feed',

    { params: { nextPage: this.nextPage } }).pipe( catchError(() => EMPTY) );
  20. private nextPage: number | null = 1; feed$ = this.http.get('/feed',

    { params: { nextPage: this.nextPage } }).pipe( catchError(() => EMPTY) );
  21. private nextPage: number | null = 1; private getLoadObservable: ()

    => Observable<FakeFeedResponse> = () => { return this.http.get('/feed', { params: { nextPage: this.nextPage } }).pipe( catchError(() => EMPTY) ) };
  22. private nextPage: number | null = 1; private getLoadObservable: ()

    => Observable<FakeFeedResponse> = () => { return this.nextPage ? this.http.get('/feed', { params: { nextPage: this.nextPage } }).pipe( catchError(() => EMPTY) ) : EMPTY; };
  23. private nextPage: number | null = 1; private getLoadObservable: ()

    => Observable<FakeFeedResponse> = () => { return this.nextPage ? this.http.get('/feed', { params: { nextPage: this.nextPage } }).pipe( catchError(() => EMPTY) , tap({ next: ({ nextPage }) => this.nextPage = nextPage }) ) : EMPTY; };
  24. private nextPage: number | null = 1; private getLoadObservable: ()

    => Observable<FakeFeedResponse> = () => { return this.nextPage ? this.http.get('/feed', { params: { nextPage: this.nextPage } }).pipe( catchError(() => EMPTY), tap({ next: ({ nextPage }) => this.nextPage = nextPage }) ) : EMPTY; }; feed$: Observable<FakeFeedResponse> = this.getLoadObservable();
  25. private nextPage: number | null = 1; private getLoadObservable: ()

    => Observable<FakeFeedResponse> = () => { return this.nextPage ? this.http.get('/feed', { params: { nextPage: this.nextPage } }).pipe( catchError(() => EMPTY), tap({ next: ({ nextPage }) => this.nextPage = nextPage }) ) : EMPTY; }; feed$: Observable<FakeFeedResponse> = this.getLoadObservable();
  26. private nextPage: number | null = 1; private getLoadObservable: ()

    => Observable<FakeFeedResponse> = () => { return this.nextPage ? this.http.get('/feed', { params: { nextPage: this.nextPage } }).pipe( catchError(() => EMPTY), tap({ next: ({ nextPage }) => this.nextPage = nextPage }) ) : EMPTY; }; loadMore$ = new BehaviorSubject(null); feed$: Observable<FakeFeedResponse> = this.getLoadObservable();
  27. private nextPage: number | null = 1; private getLoadObservable: ()

    => Observable<FakeFeedResponse> = () => { return this.nextPage ? this.http.get('/feed', { params: { nextPage: this.nextPage } }).pipe( catchError(() => EMPTY), tap({ next: ({ nextPage }) => this.nextPage = nextPage }) ) : EMPTY; }; loadMore$ = new BehaviorSubject(null); feed$: Observable<FakeFeedResponse> = this.loadMore$.pipe( exhaustMap(() => this.getLoadObservable() ) );
  28. private nextPage: number | null = 1; private getLoadObservable: ()

    => Observable<FakeFeedResponse> = () => { return this.nextPage ? this.http.get('/feed', { params: { nextPage: this.nextPage } }).pipe( catchError(() => EMPTY), tap({ next: ({ nextPage }) => this.nextPage = nextPage }) ) : EMPTY; }; loadMore$ = new BehaviorSubject(null); feed$: Observable<FakeFeedResponse> = this.loadMore$.pipe( exhaustMap(() => this.getLoadObservable()) );
  29. class FeedComponent { feed$: Observable<FeedItem[]> = this.feedService.feed$.pipe( map(({ items })

    => items) ); constructor(private feedService: FeedService) { } }
  30. class FeedComponent { feed$: Observable<FeedItem[]> = this.feedService.feed$.pipe( map(({ items })

    => items) ); constructor(private feedService: FeedService) { } }
  31. class FeedComponent { feed$: Observable<FeedItem[]> = this.feedService.feed$.pipe( map(({ items })

    => items) ); @ViewChildren('article') articles: QueryList<ElementRef<HTMLElement>>; constructor(private feedService: FeedService) { } }
  32. <div class="articles"> <article #article [id]="item.id" *ngFor="let item of feed$ |

    async"> <div class="user"> <img [src]="item.user.avatar" [alt]="item.user.name + ' avatar'"> {{item.user.name}} </div> <p *ngIf="item.type === 'text'">{{item.text}}</p> <img *ngIf="item.type === 'image'" [src]="item.imageURL" [alt]="item.imageURL"> </article> </div>
  33. class FeedComponent { feed$: Observable<FeedItem[]> = this.feedService.feed$.pipe( map(({ items })

    => items) ); @ViewChildren('article') articles: QueryList<ElementRef<HTMLElement>>; constructor(private feedService: FeedService) { } }
  34. class FeedComponent implements AfterViewInit { feed$: Observable<FeedItem[]> = this.feedService.feed$.pipe( map(({

    items }) => items) ); @ViewChildren('article') articles: QueryList<ElementRef<HTMLElement>>; constructor(private feedService: FeedService) { } ngAfterViewInit() { } }
  35. class FeedComponent implements AfterViewInit { feed$: Observable<FeedItem[]> = this.feedService.feed$.pipe( map(({

    items }) => items) ); @ViewChildren('article') articles: QueryList<ElementRef<HTMLElement>>; ngAfterViewInit() { } }
  36. class FeedComponent implements AfterViewInit { feed$: Observable<FeedItem[]> = this.feedService.feed$.pipe( map(({

    items }) => items) ); @ViewChildren('article') articles: QueryList<ElementRef<HTMLElement>>; ngAfterViewInit() { this.articles.changes } }
  37. class FeedComponent implements AfterViewInit { feed$: Observable<FeedItem[]> = this.feedService.feed$.pipe( map(({

    items }) => items) ); @ViewChildren('article') articles: QueryList<ElementRef<HTMLElement>>; ngAfterViewInit() { this.articles.changes .pipe( ); } }
  38. class FeedComponent implements AfterViewInit { feed$: Observable<FeedItem[]> = this.feedService.feed$.pipe( map(({

    items }) => items) ); @ViewChildren('article') articles: QueryList<ElementRef<HTMLElement>>; ngAfterViewInit() { this.articles.changes.pipe( map( ) ); } }
  39. class FeedComponent implements AfterViewInit { feed$: Observable<FeedItem[]> = this.feedService.feed$.pipe( map(({

    items }) => items) ); @ViewChildren('article') articles: QueryList<ElementRef<HTMLElement>>; ngAfterViewInit() { this.articles.changes.pipe( map(queryList => queryList ) ); } }
  40. class FeedComponent implements AfterViewInit { feed$: Observable<FeedItem[]> = this.feedService.feed$.pipe( map(({

    items }) => items) ); @ViewChildren('article') articles: QueryList<ElementRef<HTMLElement>>; ngAfterViewInit() { this.articles.changes.pipe( map(queryList => queryList .toArray() ) ); } }
  41. class FeedComponent implements AfterViewInit { feed$: Observable<FeedItem[]> = this.feedService.feed$.pipe( map(({

    items }) => items) ); @ViewChildren('article') articles: QueryList<ElementRef<HTMLElement>>; ngAfterViewInit() { this.articles.changes.pipe( map(queryList => queryList .toArray() .splice(-3) ) ); } }
  42. class FeedComponent implements AfterViewInit { feed$: Observable<FeedItem[]> = this.feedService.feed$.pipe( map(({

    items }) => items) ); @ViewChildren('article') articles: QueryList<ElementRef<HTMLElement>>; ngAfterViewInit() { this.articles.changes.pipe( map(queryList => queryList .toArray() .splice(-3) .map(({ nativeElement }) => nativeElement) ) ); } }
  43. class FeedComponent implements AfterViewInit { feed$: Observable<FeedItem[]> = this.feedService.feed$.pipe( map(({

    items }) => items) ); @ViewChildren('article') articles: QueryList<ElementRef<HTMLElement>>; ngAfterViewInit() { this.articles.changes.pipe( map(queryList => queryList .toArray() .splice(-3) .map(({ nativeElement }) => nativeElement) ) ); } }
  44. function fromIntersectionObserver ( target: Element, options?: IntersectionObserverInit ): Observable<IntersectionObserverEntry> {

    return new Observable(subscriber => { const callback = ([entry]: IntersectionObserverEntry[]) => { subscriber. next(entry); }; const io = new IntersectionObserver (callback, options); io.observe(target); return () => io.disconnect(); }); }
  45. class FeedComponent implements AfterViewInit { feed$: Observable<FeedItem[]> = this.feedService.feed$.pipe( map(({

    items }) => items) ); @ViewChildren('article') articles: QueryList<ElementRef<HTMLElement>>; ngAfterViewInit() { this.articles.changes.pipe( map(queryList => queryList .toArray() .splice(-3) .map(({ nativeElement }) => nativeElement) ), ); } }
  46. class FeedComponent implements AfterViewInit { feed$: Observable<FeedItem[]> = this.feedService.feed$.pipe( map(({

    items }) => items) ); @ViewChildren('article') articles: QueryList<ElementRef<HTMLElement>>; ngAfterViewInit() { this.articles.changes.pipe( map(queryList => queryList .toArray() .splice(-3) .map(({ nativeElement }) => nativeElement) ), switchMap(elements => ) ); } }
  47. class FeedComponent implements AfterViewInit { feed$: Observable<FeedItem[]> = this.feedService.feed$.pipe( map(({

    items }) => items) ); @ViewChildren('article') articles: QueryList<ElementRef<HTMLElement>>; ngAfterViewInit() { this.articles.changes.pipe( map(queryList => queryList .toArray() .splice(-3) .map(({ nativeElement }) => nativeElement) ), switchMap(elements => elements.map(el => fromIntersectionObserver(el)) ) ); } }
  48. class FeedComponent implements AfterViewInit { feed$: Observable<FeedItem[]> = this.feedService.feed$.pipe( map(({

    items }) => items) ); @ViewChildren('article') articles: QueryList<ElementRef<HTMLElement>>; ngAfterViewInit() { this.articles.changes.pipe( map(queryList => queryList .toArray() .splice(-3) .map(({ nativeElement }) => nativeElement) ), switchMap(elements => elements.map(el => fromIntersectionObserver(el)) ) ); } }
  49. class FeedComponent implements AfterViewInit { feed$: Observable<FeedItem[]> = this.feedService.feed$.pipe( map(({

    items }) => items) ); @ViewChildren('article') articles: QueryList<ElementRef<HTMLElement>>; ngAfterViewInit() { this.articles.changes.pipe( map(queryList => queryList .toArray() .splice(-3) .map(({ nativeElement }) => nativeElement) ), switchMap(elements => from(elements.map(el => fromIntersectionObserver(el)) ) ) ); } }
  50. class FeedComponent implements AfterViewInit { feed$: Observable<FeedItem[]> = this.feedService.feed$.pipe( map(({

    items }) => items) ); @ViewChildren('article') articles: QueryList<ElementRef<HTMLElement>>; ngAfterViewInit() { this.articles.changes.pipe( map(queryList => queryList .toArray() .splice(-3) .map(({ nativeElement }) => nativeElement) ), switchMap(elements => from(elements.map(el => fromIntersectionObserver(el))) ) ); } }
  51. class FeedComponent implements AfterViewInit { feed$: Observable<FeedItem[]> = this.feedService.feed$.pipe( map(({

    items }) => items) ); @ViewChildren('article') articles: QueryList<ElementRef<HTMLElement>>; ngAfterViewInit() { this.articles.changes.pipe( map(queryList => queryList .toArray() .splice(-3) .map(({ nativeElement }) => nativeElement) ), switchMap(elements => from(elements.map(el => fromIntersectionObserver(el))) .pipe(mergeAll()) ) ); } }
  52. class FeedComponent implements AfterViewInit { feed$: Observable<FeedItem[]> = this.feedService.feed$.pipe( map(({

    items }) => items) ); @ViewChildren('article') articles: QueryList<ElementRef<HTMLElement>>; ngAfterViewInit() { this.articles.changes.pipe( map(queryList => queryList .toArray() .splice(-3) .map(({ nativeElement }) => nativeElement) ), switchMap(elements => from(elements.map(el => fromIntersectionObserver(el))).pipe( mergeMap(x => x)) ) ); } }
  53. class FeedComponent implements AfterViewInit { feed$: Observable<FeedItem[]> = this.feedService.feed$.pipe( map(({

    items }) => items) ); @ViewChildren('article') articles: QueryList<ElementRef<HTMLElement>>; ngAfterViewInit() { this.articles.changes.pipe( map(queryList => queryList .toArray() .splice(-3) .map(({ nativeElement }) => nativeElement) ), switchMap(elements => from(elements.map(el => fromIntersectionObserver(el))).pipe(mergeAll()) ) ); } }
  54. class FeedComponent implements AfterViewInit { feed$: Observable<FeedItem[]> = this.feedService.feed$.pipe( map(({

    items }) => items) ); @ViewChildren('article') articles: QueryList<ElementRef<HTMLElement>>; ngAfterViewInit() { this.articles.changes.pipe( map(queryList => queryList .toArray() .splice(-3) .map(({ nativeElement }) => nativeElement) ), switchMap(elements => from(elements.map(el => fromIntersectionObserver(el))).pipe(mergeAll()) ), filter(({ isIntersecting }) => isIntersecting) ); } }
  55. private nextPage: number | null = 1; private getLoadObservable: ()

    => Observable<FakeFeedResponse> = () => { return this.nextPage ? this.http.get('/feed', { params: { nextPage: this.nextPage } }).pipe( catchError(() => EMPTY), tap({ next: ({ nextPage }) => this.nextPage = nextPage }) ) : EMPTY; }; loadMore$ = new BehaviorSubject(null); feed$: Observable<FakeFeedResponse> = this.loadMore$.pipe( exhaustMap(() => this.getLoadObservable()) );
  56. class FeedComponent implements AfterViewInit { feed$: Observable<FeedItem[]> = this.feedService.feed$.pipe( map(({

    items }) => items) ); @ViewChildren('article') articles: QueryList<ElementRef<HTMLElement>>; ngAfterViewInit() { this.articles.changes.pipe( map(queryList => queryList .toArray() .splice(-3) .map(({ nativeElement }) => nativeElement) ), switchMap(elements => from(elements.map(el => fromIntersectionObserver(el))).pipe(mergeAll()) ), filter(({ isIntersecting }) => isIntersecting) ); } }
  57. class FeedComponent implements AfterViewInit { feed$: Observable<FeedItem[]> = this.feedService.feed$.pipe( map(({

    items }) => items) ); @ViewChildren('article') articles: QueryList<ElementRef<HTMLElement>>; ngAfterViewInit() { this.articles.changes.pipe( map(queryList => queryList .toArray() .splice(-3) .map(({ nativeElement }) => nativeElement) ), switchMap(elements => from(elements.map(el => fromIntersectionObserver(el))).pipe(mergeAll()) ), filter(({ isIntersecting }) => isIntersecting) ).subscribe(this.feedService.loadMore$) ; } }
  58. class FeedComponent implements AfterViewInit { feed$: Observable<FeedItem[]> = this.feedService.feed$.pipe( scan((acc,

    { items }) => acc.concat(items), []) ); @ViewChildren('article') articles: QueryList<ElementRef<HTMLElement>>; ngAfterViewInit() { this.articles.changes.pipe( map(queryList => queryList .toArray() .splice(-3) .map(({ nativeElement }) => nativeElement) ), switchMap(elements => from(elements.map(el => fromIntersectionObserver(el))).pipe(mergeAll()) ), filter(({ isIntersecting }) => isIntersecting) ).subscribe(this.feedService.loadMore$); } }
  59. <div class="filter"> <form #f="ngForm"> <label> Feed options <select name="feedFilter" ngModel>

    <option value="">All</option> <option value="onlyImages">Only Images</option> <option value="onlyText">Only Text</option> </select> </label> </form> </div> <div class="articles"> <article #article [id]="item.id" *ngFor="let item of (feed$ | async)"> ... </article> </div>
  60. private nextPage: number | null = 1; private getLoadObservable: ()

    => Observable<FakeFeedResponse> = () => { return this.nextPage ? this.http.get('/feed', { params: { nextPage: this.nextPage } }).pipe( catchError(() => EMPTY), tap({ next: ({ nextPage }) => this.nextPage = nextPage }) ) : EMPTY; }; loadMore$ = new BehaviorSubject(null); feed$: Observable<FakeFeedResponse> = this.loadMore$.pipe( exhaustMap(() => this.getLoadObservable()) );
  61. private nextPage: number | null = 1; private getLoadObservable: ()

    => Observable<FakeFeedResponse> = () => { return this.nextPage ? this.http.get('/feed', { params: { nextPage: this.nextPage } }).pipe( catchError(() => EMPTY), tap({ next: ({ nextPage }) => this.nextPage = nextPage }) ) : EMPTY; }; loadMore$ = new BehaviorSubject(null); feed$: Observable<FakeFeedResponse> = this.loadMore$.pipe( exhaustMap(() => this.getLoadObservable()) );
  62. private getLoadObservable: () => Observable<FakeFeedResponse> = () => { return

    this.nextPage ? this.http.get('/feed', { params: { nextPage: this.nextPage } }).pipe( catchError(() => EMPTY), tap({ next: ({ nextPage }) => this.nextPage = nextPage }) ) : EMPTY; }; loadMore$ = new BehaviorSubject(null); feed$: Observable<FakeFeedResponse> = this.loadMore$.pipe( exhaustMap(() => this.getLoadObservable()) );
  63. private getLoadObservable: () => Observable<FakeFeedResponse> = () => { return

    this.nextPage ? this.http.get('/feed', { params: { nextPage: this.nextPage } }).pipe( catchError(() => EMPTY), tap({ next: ({ nextPage }) => this.nextPage = nextPage }) ) : EMPTY; }; loadMore$ = new BehaviorSubject(null); filterChange$ = new Subject<FeedFilterType>(); feed$: Observable<FakeFeedResponse> = this.loadMore$.pipe( exhaustMap(() => this.getLoadObservable()) ); export type FeedFilterType = '' | 'onlyImages' | 'onlyText';
  64. <div class="filter"> <form #f="ngForm"> <label> Feed options <select name="feedFilter" ngModel>

    <option value="">All</option> <option value="onlyImages">Only Images</option> <option value="onlyText">Only Text</option> </select> </label> </form> </div> <div class="articles"> <article #article [id]="item.id" *ngFor="let item of (feed$ | async)"> ... </article> </div>
  65. private getLoadObservable: () => Observable<FakeFeedResponse> = () => { return

    this.nextPage ? this.http.get('/feed', { params: { nextPage: this.nextPage } }).pipe( catchError(() => EMPTY), tap({ next: ({ nextPage }) => this.nextPage = nextPage }) ) : EMPTY; }; loadMore$ = new BehaviorSubject(null); filterChange$ = new Subject<FeedFilterType>(); feed$: Observable<FakeFeedResponse> = this.loadMore$.pipe( exhaustMap(() => this.getLoadObservable()) );
  66. private getLoadObservable: () => Observable<FakeFeedResponse> = () => { return

    this.nextPage ? this.http.get('/feed', { params: { nextPage: this.nextPage } }).pipe( catchError(() => EMPTY), tap({ next: ({ nextPage }) => this.nextPage = nextPage }) ) : EMPTY; }; loadMore$ = new BehaviorSubject(null); filterChange$ = new Subject<FeedFilterType>(); feed$: Observable<FakeFeedResponse> = this.filterChange$.pipe( switchMap(feedFilter => this.loadMore$.pipe( exhaustMap(() => this.getLoadObservable()) ) ) );
  67. private getLoadObservable: () => Observable<FakeFeedResponse> = () => { return

    this.nextPage ? this.http.get('/feed', { params: { nextPage: this.nextPage } }).pipe( catchError(() => EMPTY), tap({ next: ({ nextPage }) => this.nextPage = nextPage }) ) : EMPTY; }; loadMore$ = new BehaviorSubject(null); filterChange$ = new Subject<FeedFilterType>(); feed$: Observable<FakeFeedResponse> = this.filterChange$.pipe( switchMap(feedFilter => { this.nextPage = 1; return this.loadMore$.pipe( exhaustMap(() => this.getLoadObservable()) ); }) );
  68. private getLoadObservable: () => Observable<FakeFeedResponse> = () => { return

    this.nextPage ? this.http.get('/feed', { params: { nextPage: this.nextPage } }).pipe( catchError(() => EMPTY), tap({ next: ({ nextPage }) => this.nextPage = nextPage }) ) : EMPTY; }; loadMore$ = new BehaviorSubject(null); filterChange$ = new Subject<FeedFilterType>(); feed$: Observable<FakeFeedResponse> = this.filterChange$.pipe( switchMap(feedFilter => { this.nextPage = 1; return this.loadMore$.pipe( exhaustMap(() => this.getLoadObservable( feedFilter)) ); }) );
  69. private getLoadObservable = () => { return this.nextPage ? this.http.get('/feed',

    { params: { nextPage: this.nextPage } }).pipe( catchError(() => EMPTY), tap({ next: ({ nextPage }) => this.nextPage = nextPage }) ) : EMPTY; }; loadMore$ = new BehaviorSubject(null); filterChange$ = new Subject<FeedFilterType>(); feed$: Observable<FakeFeedResponse> = this.filterChange$.pipe( switchMap(feedFilter => { this.nextPage = 1; return this.loadMore$.pipe( exhaustMap(() => this.getLoadObservable( feedFilter)) ); }) );
  70. private getLoadObservable = ( feedFilter: FeedFilterType ) => { return

    this.nextPage ? this.http.get('/feed', { params: { nextPage: this.nextPage , feedFilter } }).pipe( catchError(() => EMPTY), tap({ next: ({ nextPage }) => this.nextPage = nextPage }) ) : EMPTY; }; loadMore$ = new BehaviorSubject(null); filterChange$ = new Subject<FeedFilterType>(); feed$: Observable<FakeFeedResponse> = this.filterChange$.pipe( switchMap(feedFilter => { this.nextPage = 1; return this.loadMore$.pipe( exhaustMap(() => this.getLoadObservable(feedFilter)) ); }) );
  71. class FeedComponent implements AfterViewInit { feed$: Observable<FeedItem[]> = this.feedService.feed$.pipe( scan((acc,

    { items }) => acc.concat(items), []) ); @ViewChildren('article') articles: QueryList<ElementRef<HTMLElement>>; ngAfterViewInit() { this.articles.changes.pipe( map(queryList => queryList .toArray() .splice(-3) .map(({ nativeElement }) => nativeElement) ), switchMap(elements => from(elements.map(el => fromIntersectionObserver(el))).pipe(mergeAll()) ), filter(({ isIntersecting }) => isIntersecting) ).subscribe(this.feedService.loadMore$); } }
  72. class FeedComponent implements AfterViewInit { feed$: Observable<FeedItem[]> = this.feedService.feed$.pipe( scan((acc,

    { items , page }) => page === 1 ? items : acc.concat(items), []) ); @ViewChildren('article') articles: QueryList<ElementRef<HTMLElement>>; ngAfterViewInit() { this.articles.changes.pipe( map(queryList => queryList .toArray() .splice(-3) .map(({ nativeElement }) => nativeElement) ), switchMap(elements => from(elements.map(el => fromIntersectionObserver(el))).pipe(mergeAll()) ), filter(({ isIntersecting }) => isIntersecting) ).subscribe(this.feedService.loadMore$); } }
  73. class FeedComponent implements AfterViewInit { feed$: Observable<FeedItem[]> = this.feedService.feed$.pipe( scan((acc,

    { items, page }) => page === 1 ? items : acc.concat(items), []) ); @ViewChild('f') form: NgForm; @ViewChildren('article') articles: QueryList<ElementRef<HTMLElement>>; ngAfterViewInit() { this.articles.changes.pipe( map(queryList => queryList .toArray() .splice(-3) .map(({ nativeElement }) => nativeElement) ), switchMap(elements => from(elements.map(el => fromIntersectionObserver(el))).pipe(mergeAll()) ), filter(({ isIntersecting }) => isIntersecting) ).subscribe(this.feedService.loadMore$); }
  74. <div class="filter"> <form #f="ngForm"> <label> Feed options <select name=" feedFilter"

    ngModel> <option value="">All</option> <option value="onlyImages">Only Images</option> <option value="onlyText">Only Text</option> </select> </label> </form> </div> <div class="articles"> <article #article [id]="item.id" *ngFor="let item of (feed$ | async)"> ... </article> </div>
  75. class FeedComponent implements AfterViewInit { feed$: Observable<FeedItem[]> = this.feedService.feed$.pipe( scan((acc,

    { items, page }) => page === 1 ? items : acc.concat(items), []) ); @ViewChild('f') form: NgForm; @ViewChildren('article') articles: QueryList<ElementRef<HTMLElement>>; ngAfterViewInit() { this.articles.changes.pipe( // ... } }
  76. class FeedComponent implements AfterViewInit { feed$: Observable<FeedItem[]> = this.feedService.feed$.pipe( scan((acc,

    { items, page }) => page === 1 ? items : acc.concat(items), []) ); @ViewChild('f') form: NgForm; @ViewChildren('article') articles: QueryList<ElementRef<HTMLElement>>; ngAfterViewInit() { this.articles.changes.pipe( // ... this.form.valueChanges } }
  77. class FeedComponent implements AfterViewInit { feed$: Observable<FeedItem[]> = this.feedService.feed$.pipe( scan((acc,

    { items, page }) => page === 1 ? items : acc.concat(items), []) ); @ViewChild('f') form: NgForm; @ViewChildren('article') articles: QueryList<ElementRef<HTMLElement>>; ngAfterViewInit() { this.articles.changes.pipe( // ... this.form.valueChanges .pipe( map(({ feedFilter }) => feedFilter) ); } }
  78. class FeedComponent implements AfterViewInit { feed$: Observable<FeedItem[]> = this.feedService.feed$.pipe( scan((acc,

    { items, page }) => page === 1 ? items : acc.concat(items), []) ); @ViewChild('f') form: NgForm; @ViewChildren('article') articles: QueryList<ElementRef<HTMLElement>>; ngAfterViewInit() { this.articles.changes.pipe( // ... this.form.valueChanges.pipe( map(({ feedFilter }) => feedFilter) ).subscribe(this.feedService.filterChange$) ; } }
  79. class FeedComponent implements AfterViewInit { feed$: Observable<FeedItem[]> = this.feedService.feed$.pipe( scan((acc,

    { items, page }) => page === 1 ? items : acc.concat(items), []) ); @ViewChild('f') form: NgForm; @ViewChildren('article') articles: QueryList<ElementRef<HTMLElement>>; ngAfterViewInit() { this.articles.changes.pipe( // ... this.form.valueChanges.pipe( map(({ feedFilter }) => feedFilter) ).subscribe(this.feedService.filterChange$); } }
  80. <div class="loading" *ngIf="loading$ | async"> Loading... </div> <div class="filter"> <form

    #f="ngForm"> ... </form> </div> <div class="articles"> <article #article [id]="item.id" *ngFor="let item of (feed$ | async)"> ... </article> </div>
  81. class FeedComponent implements AfterViewInit { loading$: Observable<boolean> = this.feedService.loading$; feed$:

    Observable<FeedItem[]> = this.feedService.feed$.pipe( scan((acc, { items, page }) => page === 1 ? items : acc.concat(items), []) ); @ViewChild('f') form: NgForm; @ViewChildren('article') articles: QueryList<ElementRef<HTMLElement>>; ngAfterViewInit() { this.articles.changes.pipe( // ... this.form.valueChanges.pipe( map(({ feedFilter }) => feedFilter) ).subscribe(this.feedService.filterChange$); } }
  82. private loadingChange$ = new Subject<boolean>(); private getLoadObservable = (feedFilter: FeedFilterType)

    => { return this.nextPage ? this.http.get('/feed', { params: { nextPage: this.nextPage, feedFilter } }).pipe( catchError(() => EMPTY), tap({ next: ({ nextPage }) => this.nextPage = nextPage }) ) : EMPTY; }; loadMore$ = new BehaviorSubject(null); filterChange$ = new Subject<FeedFilterType>(); feed$: Observable<FakeFeedResponse> = this.filterChange$.pipe( ... );
  83. private loadingChange$ = new Subject<boolean>(); private getLoadObservable = (feedFilter: FeedFilterType)

    => { return this.nextPage ? this.http.get('/feed', { params: { nextPage: this.nextPage, feedFilter } }).pipe( catchError(() => EMPTY), tap({ subscribe: () => this.loadingChange$.next(true), next: ({ nextPage }) => this.nextPage = nextPage, finalize: () => this.loadingChange$.next(false) }) ) : EMPTY; }; loadMore$ = new BehaviorSubject(null); filterChange$ = new Subject<FeedFilterType>(); feed$: Observable<FakeFeedResponse> = this.filterChange$.pipe( ... );
  84. private loadingChange$ = new Subject<boolean>(); private getLoadObservable = (feedFilter: FeedFilterType)

    => { return this.nextPage ? this.http.get('/feed', { params: { nextPage: this.nextPage, feedFilter } }).pipe( catchError(() => EMPTY), tap({ subscribe: () => this.loadingChange$.next(true), next: ({ nextPage }) => this.nextPage = nextPage, finalize: () => this.loadingChange$.next(false) }) ) : EMPTY; }; loadMore$ = new BehaviorSubject(null); filterChange$ = new Subject<FeedFilterType>(); feed$: Observable<FakeFeedResponse> = this.filterChange$.pipe( ... ); loading$: Observable<boolean> = this.loadingChange$.asObservable();
  85. Summary • RxJS is best used for coordination of multiple

    events ◦ I loaded data from server and I tracked loading state ◦ I handled events from QueryList (I suggest using MutationObserver which is natively supported by modern browsers) ◦ I subscribed to IntersectionObserver events ◦ I tracked changes in filters • Work backwards ◦ feed$ Observable was written once and extended as needed ◦ loadMore$ and filterChange$ Subject were added ◦ and loading$ Observable in the end • Factor out your Observables - there's no need to use factory functions (like I did here) • But, what about resources? ◦ async pipe ◦ takeUntil (just in case)
  86. Thank you • Demo project: ◦ https://github.com/jakovljevic-mladen/ng-pagination-rx-streams • The StackOverflow

    question ◦ https://stackoverflow.com/q/60155426/1253279 • If you'd like to contribute to RxJS-u: ◦ https://github.com/ReactiveX/rxjs • My Twitter (quite inactive) profile: ◦ https://twitter.com/jakovljevicMla