The original link: blog. Strongbrew. IO/infinite – sc…

This article is translated by RxJS Chinese community, if you need to reprint, please indicate the source, thank you for your cooperation!

If you would like to translate more quality RxJS articles with us, please click here.

About this article

This article shows you how to use “responsive programming” to create a great infinite scrolling load list with very little code. For this article, we’ll use RxJS and Angular. If RxJS is new to you, it’s a good idea to read the official documentation first. But using Angular or React doesn’t affect the flow of this article.

Responsive programming

Reactive programming has the following advantages over imperative programming:

  • No more “if xx, else xx” scenarios
  • Can forget a lot of marginal cases
  • It is easy to separate presentation layer logic from other logic (presentation layer only responds to flow)
  • Standard in itself: supported by a wide range of languages
  • Once these concepts are understood, complex logic can be implemented in a very simple way with very little code

A few days ago, a colleague of mine came to me with a problem: he wanted to implement infinite scroll loading in Angular, but he had stumbled on the boundaries of imperative programming. As it turns out, the infinite scroll load solution is actually a great example of how responsive programming can help you write better code.

What should infinite scroll loading look like?

An infinite scroll load list asynchronously loads data after the user scrolls the page to a specified location. This is a great way to avoid unsolicited loading (which requires the user to click every time), and it really maintains the performance of your application. It is also an effective way to reduce bandwidth and enhance user experience.

For this scenario, let’s say each page contains 10 pieces of data, and all of the data is displayed in a long scrollable list, which is an infinite scroll load list.

Let’s list the features that the infinite scroll loading list must satisfy:

  • The first page of data should be loaded by default
  • If the data on the first page does not fill the first screen, load the data on the second page, and so on, until the first screen is full
  • As the user scrolls down, the third page of data should be loaded, and so on
  • When the user resizes the window and has more room to display the results, the next page of data should be loaded
  • You should ensure that the same page of data is not loaded twice (caching)

The first drawing

Like most coding decisions, it’s a good idea to draw on a whiteboard first. This may be personal, but it helps me write code that doesn’t get deleted or refactored at a later stage.

According to the list of features above, there are three actions that can cause an application to trigger data loading: scroll, resize the window, and manually trigger data loading. When we think in a responsive way, we can find three sources of events, which we call flows:

  • Stream of scroll events: scroll$
  • Resize stream of events: resize$
  • The stream that manually determines which pages of data to load: pageByManual$

Note: We suffix the stream variable $to indicate that it is a stream, which is a convention (personally, I prefer it this way)

Let’s draw these flows on the whiteboard:

Over time, these streams will contain specific values:

The Scroll $stream contains the Y value, which is used to calculate the page number.

The resize$stream contains the event value. We don’t need the value itself, but we do need to know that the user resized the window.

PageByManual $contains the page number because it is a Subject, so we can set it directly. (More on that later)

What if we could map all of these streams to page-numbered streams? That’s good, because the data for a given page can only be loaded based on the page number. So how do you map the current stream to a page-numbered stream? That’s not something we need to think about right now (we’re just drawing, remember?). . The next graph looks something like this:

As you can see from the figure, we created the following flow based on the initial flow:

  • PageByScroll $: contains the page number based on the scroll event
  • PageByResize $: contains the page number based on the resize event
  • PageByManual $: contains page numbers based on manual events (for example, if there is still space on the page, we need to load the next page data)

If we were able to merge these three page-number streams in an efficient manner, we would have a new stream called pageToLoad$that contains the page numbers created by the Scroll event, resize event, and manual event.

If we subscribe to the pageToLoad$stream without getting data from the service, then our infinite scroll load is already partially working. But aren’t we supposed to think in responsive ways? That means avoiding subscriptions whenever possible… In fact, we need to create a new stream based on the pageToLoad$stream that will contain the data from the infinitely scrolling load list…

Now combine these drawings into a comprehensive design.

As shown, we have three input streams: they handle scrolling, window resizing, and manual triggering, respectively. We then have three page-number streams based on the input stream and merge them into a single stream, the pageToLoad$stream. Based on the pageToLoad$stream, we can get the data.

Start coding

Now that we’ve drawn enough, and we have a clear idea of what to do with an infinite scrolling load list, let’s start coding.

To figure out which page to load, we need two properties:

private itemHeight = 40;
private numberOfItems = 10; // The number of items on the page
Copy the code

pageByScroll$

PageByScroll $stream looks like this:

private pageByScroll$ = 
  // First, we create a stream that contains all the scrolling events that occur on the window object
	Observable.fromEvent(window."scroll") 
  // We are only interested in the scrollY values of these events
  // So create a stream that contains only these values
	.map((a)= > window.scrollY)
  // Create a stream containing only filter values
  // We just need the value when we scroll outside the viewport
	.filter(current= > current >=  document.body.clientHeight - window.innerHeight)
  // Only after the user stops scrolling for 200ms will we continue
  // So add 200ms debounce time for this stream
	.debounceTime(200) 
  // Filter out duplicate values
	.distinct() 
  // Count the page number
	.map(y= > Math.ceil((y + window.innerHeight)/ (this.itemHeight * this.numberOfItems)));
	
	/ / -- -- -- -- -- -- -- -- 1 -- 2 -- 3 -- -- -- -- -- - 2...
Copy the code

Note: In a real application, you might want to use the injection services for Window and Document

pageByResize$

PageByResize $stream looks like this:

  private pageByResize$ = 
  // Now we will create a stream that contains all resize events that occur on the window object
	Observable.fromEvent(window."resize")
  // When the user stops the operation for 200ms, we can continue the operation
	.debounceTime(200) 
  // Calculate the page number based on window
   .map(_ => Math.ceil(
	   	(window.innerHeight + document.body.scrollTop) / 
	   	(this.itemHeight * this.numberOfItems)
   	));
   
	/ / -- -- -- -- -- -- -- -- 1 -- 2 -- 3 -- -- -- -- -- - 2...
Copy the code

pageByManual$

The pageByManual$stream is used to get the initial value (the first screen data), but it also requires manual control. The BehaviorSubject is perfect because we need a flow with initial values that we can add manually.

private pageByManual$ = new BehaviorSubject(1);

/ / 1-2 -- 3 -- -- -- -- -- -...
Copy the code

pageToLoad$

Cool, now that we have a 3-page input stream, let’s create the pageToLoad$stream.

private pageToLoad$ = 
  // Merge all page number streams into a new stream
	Observable.merge(this.pageByManual$, this.pageByScroll$, this.pageByResize$)
  // Filter out duplicate values
	.distinct() 
  // Check whether the current page number exists in the cache (i.e. an array property in the component)
	.filter(page= > this.cache[page- 1= = =undefined); 
Copy the code

itemResults$

The hard part is done. Now we have a stream with a page number, which is very useful. We no longer need to care about individual scenarios or other complex logic. Every time the pageToLoad$stream has a new value, we just load the data. It’s that simple!!

We’ll use the FlatMap operator to do this, since the call data itself returns a stream. FlatMap (or MergeMap) flattens a higher-order Observable.

itemResults$ = this.pageToLoad$ 
  // Load data asynchronously based on page stream
  FlatMap is an alias for meregMap
	.flatMap((page: number) = > {
    // Load some Star Wars characters
		return this.http.get(`https://swapi.co/api/people? page=${page}`)
      // Create a stream containing the data
			.map(resp= > resp.json().results)
			.do(resp= > {
        // Add the page number to the cache
				this.cache[page - 1] = resp;
        // if the page still has enough blank space, continue to load data :)
				if((this.itemHeight * this.numberOfItems * page) < window.innerHeight){
					this.pageByManual$.next(page + 1); }})})// Finally, only the stream containing the data cache is returned
	.map(_ => flatMap(this.cache)); 
Copy the code

The results of

The complete code looks like this:

Note that async Pipe is responsible for the entire subscription process.

@Component({
  selector: 'infinite-scroll-list',
  template: ` 
      
`
}) export class InfiniteScrollListComponent { private cache = []; private pageByManual$ = new BehaviorSubject(1); private itemHeight = 40; private numberOfItems = 10; private pageByScroll$ = Observable.fromEvent(window."scroll") .map((a)= > window.scrollY) .filter(current= > current >= document.body.clientHeight - window.innerHeight) .debounceTime(200) .distinct() .map(y= > Math.ceil((y + window.innerHeight)/ (this.itemHeight * this.numberOfItems))); private pageByResize$ = Observable.fromEvent(window."resize") .debounceTime(200) .map(_ => Math.ceil( (window.innerHeight + document.body.scrollTop) / (this.itemHeight * this.numberOfItems) )); private pageToLoad$ = Observable .merge(this.pageByManual$, this.pageByScroll$, this.pageByResize$) .distinct() .filter(page= > this.cache[page- 1= = =undefined); itemResults$ = this.pageToLoad$ .do(_ => this.loading = true) .flatMap((page: number) = > { return this.http.get(`https://swapi.co/api/people? page=${page}`) .map(resp= > resp.json().results) .do(resp= > { this.cache[page - 1] = resp; if((this.itemHeight * this.numberOfItems * page) < window.innerHeight){ this.pageByManual$.next(page + 1); } }) }) .map(_ => flatMap(this.cache)); constructor(private http: Http) {}}Copy the code

This is the address of the online sample. (Translator’s Note: The error does not run… Sorry)

Again (as I demonstrated in my previous article), we don’t need to use third-party solutions to solve all problems. There’s not a lot of code for scrolling through lists indefinitely, and it’s very flexible. Let’s say we want to reduce DOM stress and load 100 pieces of data at a time, so we can create a new stream to do this 🙂

Thanks for reading this article and hope you enjoy it.