Infinite Scrolling with recompose and graphql

Hey guys,

It’s been a long time since I posted something.

This post is about implementing infinite scrolling in React with the help of graphql and recompose.

Infinite scroll is UX wise a better way when compared to other approaches like pagination when you don’t know the size of your response data and also when it’s expensive to fetch the number of records in the response.

A prerequisite before going through this post is you should know React.

That was infinite scrolling and now talking about graphql which is an implementation over REST framework where the user can specify what information it needs and graphql on the server side would only send that much information. It’s really useful when your response payload from the server is huge and it also helps when you don’t want to make multiple calls to fetch data from different sources. All the browser needs to do is just make a single call specifying the set of data it needs and server side GraphQL will handle the aggregation of calls and data accordingly.

Speaking about the other tool recompose. Which provides an option to enhance your stateless functional components by providing states, handlers and lifecycle methods without the hassle of creating a stateful component. Recompose is a great tool for creating HOCs which helps in decoupling the logic from rendering. So your component would just concentrate on rendering and the Higher Order Component will handle the state, or event handling or even lifecycle methods as componentDidMount.

1xcf7x

Now coming the main topic which was infinite scrolling. First part is to add event listeners. I am going to add 2 event listeners which are on ‘scroll’ and ‘resize’ events. Now we need to figure out which is our scrolling container in order to add listeners. This depends on the structure of your HTML and CSS. On adding an event listener to scroll you have to be really careful about removing and attaching of event handlers when you’re job is done or underway. So one thing is after attaching the event listener on render it should be removed before unmounting. Also whenever we plan on the making the server query to fetch more data we should remove the event listener and add it again after the component update. So far so good.

import { lifecycle } from 'recompose';
import ListComponent from './ListComponent';

const attachListener = event => {
   scrollEl.addEventListener('scroll', handleLoadOnScoll);
   window.addEventListener('resize', handleLoadOnScoll);
}

const detachListener = event => {
   scrollEl.removeEventListener('scroll', handleLoadOnScoll);
   window.removeEventListener('resize', handleLoadOnScoll);
}

function componentDidUpdate() {
  attachListener();
}

function componentWillUnmount() {
  detachListener();
}

export default lifecycle({
  componentDidUpdate, componentWillUnmount
})(ListComponent);

So here I have a Stateless functional component called ListComponent which is wrapped under the HOC which adds the lifecycle methods on the ListComponent. Now like i said earlier I have add the event listeners for scroll and resize. And i am removing the listener before unmount. But here i am using componentDidUpdate instead of componentDidMount because i want the first set of data to be there before we can initialise the scroller.

Now graphql has a special method known as fetchMore which is made for this very purpose of loading more data. Below I am going to wire the fetchMore functionality. The handleLoadOnScoll will call the fetchMore method.

import { lifecycle, compose, withHandlers } from 'recompose';
import { graphql } from 'react-apollo';
import ListComponent from './ListComponent';
import QUERY_GQL from 'query.gql';

const attachListener = event => {
   scrollEl.addEventListener('scroll', handleLoadOnScoll);
   window.addEventListener('resize', handleLoadOnScoll);
}

const detachListener = event => {
   scrollEl.removeEventListener('scroll', handleLoadOnScoll);
   window.removeEventListener('resize', handleLoadOnScoll);
}

function componentDidUpdate() {
  attachListener();
}

function componentWillUnmount() {
  detachListener();
}

const updateCache = (previousResult, { fetchMoreResult }) => {
  ...previousResult,
  comments: [
     ...previousResult.comments,
     ...fetchMoreResult.comments,
  ]
}

const ListComponentWrapper = compose(
  withHandlers({
    handleLoadOnScoll: (data: {fetchMore, comments, loading}) => event => {
      event.preventDefault();
      const offset = scrollEl.scrollHeight - scrollEl.scrollTop - scrollEl.clientHeight;
      if (offset <= threshold && your_condition_for_cursor && !loading) {
        detachEventListener();
        fetchMore({
          variables: {
             cursor: cursorForNextLoad,
          },
          updateQuery: updateCache,
        });
      }
    }
  }),
  lifecycle({
    componentDidUpdate, componentWillUnmount
  }),
)(ListComponent);

export default graphql(QUERY_GQL, gqlConfig)(ListComponentWrapper);

Wooh. Lots of stuff to digest right. Let's go one by one. First I created a wrapper for graphQl query at the end which takes the query and graphqlconfig object and wraps the result around ListComponentWrapper which will be our enhanced component. Also a thing to note is graphql provides a boolean `loading` inside the data object of response which can be used to figure out whether the query is under processing or has executed completely. So after the query has executed the data object which has fetchMore method, the response data which here i am calling it as comments and the loading boolean is passed down to the enhanced component ListComponentWrapper.
Now lets understand the working of withHandlers HOC. withHandlers is used when you want to attach event handlers to your component. So as I had mentioned earlier that handleLoadOnScroll will be called when scroll event is executed. So now we need to calculate the scroll position at which we need to load the data. For that I have used the properties scrollHeight which is the current height of the container, scrollTop is the top position of scroll bar and clientHeight which the height of the viewframe for the element. Based on that we receive a value which can be compared with the threshold to fire the fetchMore query. I have also added another check as a custom logic to tell when to stop calling the api depending on when you reach the end of data and there is a check for data.loading should be false. So that the fetchMore call is not made multiple times.
So if all conditions are met then we remove the eventListener to avoid unnecessary queuing in the callback queue. You can read about the parameters that can be passed to fetch more here Fetch More documentation. Now there is a parameter called updateQuery which will append the new result to the initial result in the apollo cache. If you are wondering what … are for then you can read about it here. Spread syntax

Now when the call is successful the lifecycle method componentDidUpdate will be called and event listener will be attached again.

Now lets write our ListComponent.

import React from 'react';
import Loader from './Loader';

export default function ListComponent(data: {comments, loading}) {
   if(loading &amp;&amp; !comments.length) return Loader(JSX tag);
   return (
        {
         // rendering a list of comments
         <div>comments.map(comment =&gt; ( {comment.message} ))</div>
         {loading &amp;&amp; comments.length &amp;&amp; Loader(JSX tag)}
        }
   );
}

So here I am checking if loading is true and its the initial load which i am checking if comments are there or not then render the loader which can be any fancy loader that you wish to use otherwise it render the comments. But the catch here is that i have put another loader below the list which will render when loading is true and its not the initial load.
There was a problem that i had faced if I wasn’t checking for the initial load then whole page would render again and the scroll position would be reset to 0 and it will go to top which is definitely a bad experience as you have to scroll down to the bottom to see the newly loaded content.

So this is it. Although I have made this post a bit lengthy. I hope you guys found it useful. If you any suggestions or comments please post it down.

Cheers!

 

Author: shellophobia

just passing time here.. and enjoying

Leave a comment