How to combine a pagination with Observables and the AsyncPipe in Angular 9?

Viewed 1675

There's a list of products coming from an API. The products are paginated and the user can switch to another page. The simplified template looks like this:

<ul>
  <li *ngFor="let product of products$ | async">{{ product.name }}</li>
</ul>

<button type="button" (click)="gotoPage(1)">1</button>
<button type="button" (click)="gotoPage(2)">2</button>

The component does look like this:

export class ProductsComponent implements OnInit {
  products$: Observable<Product[]>;

  constructor(
    private service: ProductService
  ) { }

  ngOnInit() {
    this.products$ = this.service.getAll({page: 1});
  }

  gotoPage(page: number): void {
    this.products$ = this.service.getAll({page: page});
  }
}

My questions are: Is this the correct way to update the Obersavble? Or does this produce memory leaks?

As a note: The URL will not change and the component should not reloaded on pagination change.

4 Answers

You don't even subscribe to your observable so I don't think there is possible memory leak here, you just get some datas and the async pipe handle the 'transformation' for you.

Just in case, when you subscribe to a observable, you need to add few lines of code to properly unsubscribe and prevent memory leak :

ngUnsubscribe = new Subject();
myObservable: Observable<any>;

ngOnInit(){
  this.myObservable.pipe(takeUntil(ngUnsubscribe))
  .subscribe(...)
}

ngOnDestroy() {
  this.ngUnsubscribe.next();
  this.ngUnsubscribe.complete();
}

A subject that will allow you to trigger the correct unsubscribe on everySubscribe that has the takeUntil.

The .next and .complete are necessary because .unsubscribe does not work as expected (Noticed it at work with NGRX and found some stackoverflow thread that talk about it).

Looking at the source of Async pipe, you can see inside transform() function:

if (obj !== this._obj) {
  this._dispose();
  return this.transform(obj as any);
}

Which unsubscribes previous Observable if there is one, if it is a new object. So you are safe to use it in that way.

You can always prevent memory leaks the takeUntil + ngOnDestroy pattern.

For example,

declare a new variable private onDestroy$: Subject<void> = new Subject<void>();

this.service.getAll({page: page})
.pipe(takeUntil(this.onDestroy$))
.subscribe(... do the necessary ...);

And later, in the onDestroy() life cycle hook, you may implement the following :

 public ngOnDestroy(): void {
   this.onDestroy$.next();
   this.onDestroy$.complete() 
 }

What we have actually done is declare a new observable; Then, by using pipe method with takeUntil we inform compiler that we want to unsubscribe from the observable when any value appear in onDestroy$, Then, by using pipe method with takeUntil we inform compiler that we want to unsubscribe from the observable when any value appear in onDestroy$, thereby preventing memory leaks.

As we all know with page comes also number of items per page.

I prefer to change criteria to behavior subject and combine two observables with mergeMap

class ProductService {
  constructor() {
    this.defaultCriteria = {
      page: 0,
      pageSize: 5
    }
  }

  getAll(criteria) {
    criteria = {
      ...this.defaultCriteria,
      ...criteria
    }

    return rxjs.of(Array(criteria.pageSize).fill(0).map((pr, index) => {
      const num = index + criteria.pageSize * criteria.page + 1;
      return {
        id: num,
        name: `Product ${num}`
      }
    }))
  }

}

class ProductsComponent {
  constructor(service) {
    this.service = service;
    this.page$ = new rxjs.BehaviorSubject(0);
    this.products$ = null;
  }

  ngOnInit() {
    this.products$ = this.page$.asObservable()
      .pipe(rxjs.operators.mergeMap(page => this.service.getAll({
        page
      })))
  }

  gotoPage(page) {
    this.page$.next(page);
  }
}

const service = new ProductService();
const component = new ProductsComponent(service);
component.ngOnInit();
component.products$.subscribe(products => console.log(products));
component.gotoPage(1);
component.gotoPage(2);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.5/rxjs.umd.js"></script>

Related