retrobitguy
retrobitguy

Reputation: 585

Confusion about rxjs debounceTime

I have the this method that gets called multiple times when scrolling a table:

setViewportRange(firstRow: number, lastRow: number): void {
  this.getViewport(firstRow, lastRow).subscribe(message => {
    console.log(message);
  });
}

I don't have the control on the calling of setViewportRange method, but I need to debounce it. So I created this debouncedGetViewport method with the help lodash's debounce function:

setViewportRange(firstRow: number, lastRow: number): void {
  this.debouncedGetViewport(firstRow, lastRow);
}

debouncedGetViewport = debounce((firstRow, lastRow) => {
  return this.getViewport(firstRow, lastRow).subscribe(message => {
    console.log(message);
  });
}, 1000);

It works! But a collegue asked why I didn't use RxJs debounce instead. So I tried to implement it with RxJs but I can't get it work. The debounceTime has no effect no matter which value is passed! Can you help me to understand why this isn't working? I think I misunderstood something.

setViewportRange(firstRow: number, lastRow: number): void {
  this.debouncedGetViewport(firstRow, lastRow);
}

debouncedGetViewport = (firstRow, lastRow) => {
  return this.getViewport(firstRow, lastRow)
    .pipe(debounceTime(1000))
    .subscribe(message => {
      console.log(message);
    });
};

Thank you!

Upvotes: 2

Views: 1202

Answers (1)

Serkan Sipahi
Serkan Sipahi

Reputation: 691

First of all don`t forget to unsubscribe!

First of all make sure from memory leak perspective or strange behaviour when subscribing multiple times to this.getViewport when setViewportRange is called. You did not know what happens behind this.getViewport. It can happen that the callback of getViewport.subscribe can be called multiple times. It is good practice to always unsubscribe.

How you can unsubscribe? There are several ways for unsubscribing from an Observable but in your case you can just use the take operator.

debouncedGetViewport = debounce((firstRow, lastRow) => {
  return this.getViewport(firstRow, lastRow).pipe(take(1)).subscribe(message => {
    console.log(message);
  });
}, 1000);

Here are some resources why you should unsubscribe:

You did not exactly describe what is not working!

I created a playground based on your example issue and I think I know what do you mean with: "Can you help me to understand why this isn't working".

I guess the console.log is called but the debounceTime has no effect, right? Please make sure next time that you explain in your issue description exactly what is not working. It can happen that you will be scored with a minus point.

Why is your debounceTime not working?

I think here is a good Stack Overflow explanation from Nuno Sousa why your example with debounceTime is not working!

Consider your logic. You will create a finalized observer for each onChanges. It doesn't debounce because the observer is already finalized and debounce is to prevent emitting one, in the off-chance that another one comes. So it needs at least two emitions to be justifiable (or more ), and that can't happen if the observer is created in the callback.

It seems you are creating with this.getViewport a finalized (completed) observable which completes right after emitting the first value and thats the reason why debounceTime has here no effect.

Tip: take(1) has no effect if the observable arrives already finalized but it is a best practice to always unsubscribe the subscription.

You need a different solution!

unsubscribe$ = new Subject();
rows$: Subject<{firstRow: number, lastRow: number}> = new Subject();

ngOnInit() {
  this.rows$.pipe(
    debounceTime(500),
    switchMap(({firstRow, lastRow}) => this.getViewport(firstRow, lastRow)),
    takeUntil(unsubscribe$)
  ).subscribe(resultOfGetViewport => {
     console.log(resultOfGetViewport);
  });
}


setViewportRange(firstRow: number, lastRow: number) {
  this.rows$.next({firstRow, lastRow});
}

ngOnDestroy() {
  this.unsubscribe$.next();
  this.unsubscribe$.complete();
}

I have created for the previous code a Stackblitz example!

What is happening in our different solution?

In our new solution we do not use a finalized observable because we use a Subject (rows$) and a Subject can not complete itself as in getViewport. We must explicitly do it ourselves. We can see this in takeUntil operator. Only when the component is destroyed, so when ngOnDestroy is called we tell our rows$ observable to complete itself. Last but not least we get our value from getViewport with switchMap. Thats it.

You might wonder if the order of debounceTime and switchMap makes a difference here. It depends! If this.getViewport is an expensive operation, then place it right after debounceTime and if it is very cheap then the order doesn't matter.

Upvotes: 3

Related Questions