1

Hello,

first of all, thank you for reading this.

I want to handle scroll events stream and I want to react on scroll starts and ignore following burst of scroll events until stream considered inactive (time limit). So after delay I want repeat the same.

This is my solution so far:

    import { fromEvent } from 'rxjs';
    import { throttle, debounceTime } from 'rxjs/operators';

    const stream = fromEvent(window, 'scroll');
    const controllerStream = stream.pipe(debounceTime(500));

    this.sub = stream
      .pipe(
        throttle(() => controllerStream, {
          leading: true,
          trailing: false,
        })
      )
      .subscribe(() => {
        // react on scroll-start events
      });

Is there a better way? I was considering operators like throttleTime, debounce, debounceTime... but I could not find the configuration matching my needs

Thank you

Community
  • 1
  • 1
Luckylooke
  • 4,061
  • 4
  • 36
  • 49

2 Answers2

0

While this solution looks a bit involved, it achieves the behavior you describe, and can be encapsulated cleanly in a custom operator.

import { of, merge, NEVER } from 'rxjs';
import { share, exhaustMap, debounceTime, takeUntil } from 'rxjs/operators';

const firstAfterInactiveFor = (ms) => (source) => {
  // Multicast the source since we need to subscribe to it twice.
  const sharedSource = source.pipe(share());

  return sharedSource.pipe(
    // Ignore source until we finish the observable returned from exhaustMap's
    // callback
    exhaustMap((firstEvent) =>
      // Create an observable that emits only the initial scroll event, but never
      // completes (on its own)
      merge(of(firstEvent), NEVER).pipe(
        // Complete the never-ending observable once the source is dormant for
        // the specified duration. Once this happens, the next source event
        // will be allowed through and the process will repeat.
        takeUntil(sharedSource.pipe(debounceTime(ms)))
      )
    )
  );
};

// This achieves the desired behavior.
stream.pipe(firstAfterInactiveFor(500))

backtick
  • 2,685
  • 10
  • 18
  • Thank you for answer, it really looks quite complex comparing to my solution.. now I am wondering whether I could encapsulate my less verbose solution into custom operator :D – Luckylooke May 01 '20 at 05:36
  • please review my second solution whether you see any possible problem there, thanks :) – Luckylooke May 01 '20 at 08:18
0

I have made a third version, encapsulating my solution into custom operator based on @backtick answer. Is there a problem with this solution? Memory leak or something? I am not sure whether inner controllerStream will destroy properly or at all.

const firstAfterInactiveFor = (ms) => (source) => {
 const controllerStream = source.pipe(debounceTime(ms));
  return source
  .pipe(
    throttle(() => controllerStream, {
      leading: true,
      trailing: false
    })
  )
};

// This achieves the desired behavior.
stream
  .pipe(
    firstAfterInactiveFor(500)
  )
  .subscribe(() => {
    console.log("scroll-start");
  });

Here is codepen with comparison of all three: https://codepen.io/luckylooke/pen/zYvEoyd

EDIT: better example with logs and unsubscribe button https://codepen.io/luckylooke/pen/XWmqQBg

Luckylooke
  • 4,061
  • 4
  • 36
  • 49
  • I think the behavior of our solutions is roughly the same - I didn't have much hands-on experience with `throttle` so I misunderstood your usage. The only problem I see here is that your operator subscribes to `source` twice - once in `controllerStream` and once in the return value. See my note about multicasting. – backtick May 01 '20 at 17:45
  • From my understanding of how RxJs works I am not subscribing when I pipe Observables, so I think there is only one subscribe at the end on return value.. do you know how to debug how many active subscriptions are there? I did not find easy solution yet.. but maybe I will try those more complex I found here: https://stackoverflow.com/questions/39315707/rxjs-number-of-observable-subscriptions – Luckylooke May 09 '20 at 20:16
  • Depends how sophisticated you want to get, but there is a pretty basic way to track subscriptions - `Rx.defer`. See this [gist](https://gist.github.com/MattSPalmer/41e1aa065c576ffa8ba1a3ba722048c9). – backtick May 09 '20 at 21:22
  • To address the initial question of subscriptions, yes, there's one call that _you_ make to subscribe. But under the hood there are multiple subscriptions initiated. Because you reference `source` twice - once to debounce it and once to throttle it - it will be subscribed to twice. – backtick May 09 '20 at 21:24
  • @backtick Thanks for gist, nice and simple. And yes you're right about multiple subscription, I didn't realize the second one, but anyway I made some tests and it seems to subscribe and also unsubscribe automatically as needed. See logs: https://codepen.io/luckylooke/pen/XWmqQBg?editors=0011 So do I still need to use share operator? – Luckylooke May 11 '20 at 05:31
  • It's not a question of need, just a recommendation to be judicious with subscriptions as a matter of practice. Meaning it's not automatically a bad thing to subscribe to a stream more than once in an operator, but there's seldom an argument *in favor* of it. Your solution works, but it does subscribe twice to the source stream. See this fork: https://codepen.io/MattSPalmer/pen/pojZjrZ?editors=0011 – backtick May 12 '20 at 21:02