lampshade
lampshade

Reputation: 2796

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

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.

Upvotes: 4

Views: 2794

Answers (4)

Julius Dzidzevičius
Julius Dzidzevičius

Reputation: 11000

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.

Upvotes: 1

J&#243;zef Podlecki
J&#243;zef Podlecki

Reputation: 11283

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>

Upvotes: 0

Gauri Kesava Kumar
Gauri Kesava Kumar

Reputation: 474

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.

Upvotes: 0

Quentin Grisel
Quentin Grisel

Reputation: 4987

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

Upvotes: 1

Related Questions