Sign in
Log inSign up
Naive Infinite scroll in Reactive Programming using RxJS Observables

Naive Infinite scroll in Reactive Programming using RxJS Observables

Ashwin Sureshkumar's photo
Ashwin Sureshkumar
·Feb 25, 2017

A simple experiment to implement infinite scroll with RxJS.

What is Reactive Programming?

In simple terms it is programming with asynchronous data streams. There is a great post by Andre Staltz  —  The introduction to Reactive Programming you’ve been missing accompanied by an egghead.io video.

The introduction to Reactive Programming you’ve been missing.

https://egghead.io/courses/introduction-to-reactive-programming

What is RxJS?

RxJS or Reactive Extensions is a library developed by Microsoft Open Technologies in Javascript for transforming, composing and querying data streams.

There is a great talk by Ben Lesh on this Thinking Reactively with RxJS 5.

Below are a couple of nice introductions to Observables and a few operators by Netanel Basal.

What will we be building?

We are going to build a naive infinite scroller using observables. Whenever the user scrolls a given container to 70%, we will trigger the api call to get more data from the server. For this implementation we will use the HackerNews unofficial api to get the latest news.

Below are the operators we will be using from RxJS.

  1. map : similar to map in an array, map over the stream of incoming data.
  2. filter : similar to filter in an array, filter the stream of incoming data.
  3. pairwise : returns an array of current emitted data and also the previous emitted data.
  4. startWith: returns an observable emits supplied values before emitting the values from source observable.
  5. exhaustMap: waits to emit value until the passed in inner observable has completed

Link to the output in jsbin.com : https://output.jsbin.com/punibux

Phase 1  —  Setup basic html and styles

Import the RxJS library and we will use infinite-scroller as a scroll container and append news to it.

<!DOCTYPE html> 
<html> 
  <head> 
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width"> 
    <title>Naive Infinite Scroller - RxJS</title> 
    <script src="cdnjs.cloudflare.com/ajax/libs/rxjs/5.0.1/…"></script> 
  </head> 
  <body> 
    <ul id="infinite-scroller"></ul> 
  </body> 
</html>
#infinite-scroller { 
  height: 500px; 
  width: 700px; 
  border: 1px solid #f5ad7c; 
  overflow: scroll; 
  padding: 0; 
  li { 
    padding : 10px 5px; 
    line-height: 1.5; 
    &:nth-child(odd) { 
      background : #ffe8d8; 
    } 
    &:nth-child(even) { 
      background : #f5ad7c; 
    } 
  } 
}

Phase 2  —  Setup helpers functions for data processing, rendering and calculations

let currentPage = 1;

const getQuotesAPI = () => { 
  return 'node-hnapi.herokuapp.com/news?page=' + currentPage; 
};

// Process the data returned from the api 
const processData = res => { 
  res.json()
    .then(news => { 
        currentPage++; news.forEach(renderNews); 
    }); 
};

// Render each news on to the view 
const renderNews = (news) => {
 const li = document.createElement('li'); 
 li.innerHTML = ${news.id} - ${news.title}; 
 scrollElem.appendChild(li); 
};

// Check if the user is scrolling down by previous scroll position and current scroll position 
const isUserScrollingDown = (positions) => { 
  return positions[0].sT < positions[1].sT; 
};

//  Check if the scroll position at required percentage relative to the container 
const isScrollExpectedPercent = (position, percent) => { 
  return ((position.sT + position.cH) / position.sH) > (percent/100); 
};

First three functions are straight forward.

  1. getQuotesAPI —  returns the api url with the current page number as query param
  2. processData —  processes the return data from the api which is performed using fetch API and increase the currentPage.
  3. renderNews —  takes each news item and renders to the view.

The next two functions are the ones used for scroll calculations.

  1. isUserScrollingDown —  determines if the user is scrolling down or not.
  2. isScrollExpectedPercent —  determines if the user has scrolled to the passed in percentage to get more data.

Phase 3  —  Setup up observable stream

// Setup Stream
const scrollElem = document.getElementById('infinite-scroller');
const scrollEvent$ = Rx.Observable.fromEvent(scrollElem, 'scroll');

To capture the scroll events in the container, we need to create an observable from the scroll event. This can be achieved by using Rx.Observable.fromEvent- docs. It is a convention to append $ to the variable when referencing an observable stream.

Phase 4  —  Stream logic to process scroll events and call the api

// Stream logic
const userScrolledDown$ = scrollEvent$ 
    .map(e => ({ 
      sH: e.target.scrollHeight, 
      sT: e.target.scrollTop, 
      cH: e.target.clientHeight 
    })) 
    .pairwise() 
    .filter(positions => { 
        return isUserScrollingDown(positions) && isScrollExpectedPercent(positions[1], 70)) 
    });

const requestOnScroll$ = userScrolledDown$ 
    .startWith([]) 
    .exhaustMap(() => Rx.Observable.fromPromise(fetch(getQuotesAPI())))

// Subscribe and apply effects
requestOnScroll$.subscribe(processData);

We are going to take the scroll event emitted by scrollEvent$ and map over to take only the values we need for our infinite scroll logic. We need only three properties from the scroll element — scrollHeight, scrollTop and clientHeight.

We pass the mapped data to pairwise operator which emits the current and previous value in an array which will look like below.

Now we can take the pair of positions and pass it to the filter them to filter according to our conditions

  1. Is the user scrolling down
  2. Has the user scrolled reached seventy percent of the container

requestOnScroll$ — It gets invoked when the userScrollDown$ has passed filter conditions. We start with an initial value of an empty array.

We going to use Rx.Observable.fromPromise to create an observable from a promise. fetch makes the http call and returns a promise. exhaustMap will wait for the fetch to complete and the inner observable to emit the data from the API.

Observables are lazy  —  means they do not do anything until you subscribe to them. We will subscribe to the requestOnScroll$ and pass in processData to the subscribe method. When exhaustMap emits the data from the api it will be passed on to processData which will call renderNews to render on to the view.

Below is the gif of the infinite scroll in action, watch the scroll bar on the right.  " \" \\" \\\" \\\\" \\\\\"Infinite scroll in action\\\\\"\\\\"\\\"\\"\""

In my next post I will try to implement it in Angular 2 by creating an infinite scroll directive.

Update: Here is the link to my follow post on Simple Infinite Scroller directive with RxJS Observables


Originally published on Medium