Reputation: 2796
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
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
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
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
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