Xcoding with Alfian

Software Development Videos & Tutorials

React Apollo Client GraphQL Cursor Infinite Scroll Pagination with GitHub API

Alt text

React Apollo Client is an open source community driven React library that is designed to build web application that fetches data with GraphQL endpoint. It provides many UI component that makes querying data with GraphQL becomes so much simple and easier to integrate in our app because we don’t have to maintain the state of fetching data by ourselves. States like loading data, error, and even caching mechanism is all provided by Apollo client for us to use out of the box.

In this article, we will build a web application that uses GitHub GraphQL API to fetch repositories with most stars in recent week. GitHub GraphQL API uses Relay style schema with PageInfo for cursor based pagination, edges and node that stores the data of the query we want to retrieve. Our app will fetch repositories with pagination of 15 repositories per fetch, then we will provide Infinite Scroll behavior that will fetch more repositories when user scrolls to the bottom of the page.

What we will build:

  1. Apollo Client Setup with GitHub GraphQL API and Authentication.
  2. GraphQL query to get trending repository in recent week.
  3. Repositories Component that provide UI with infinite scroll list that displays repositories and loading indicator.
  4. Apollo Query Component integration with Repositories Component that uses cursor based pagination and Apollo onFetchMore mechanism for pagination.

React Apollo Client Setup with GitHub GraphQL API

To use React Apollo Client in our React app we need to add required dependencies to our package.json

npm install apollo-boost react-apollo graphql-tag graphql --save

Inside our main index.js file, we import ApolloClient and ApolloProvider, then we create a new ApolloClient object passing the configuration object. For uri, we assign the GitHub GraphQL API endpoint. To access the GitHub API we need to pass the Access Token to the HTTP Header Authorization. To get the Access Token, you need to sign in to GitHub and generate the Access Token from the developer settings. At last, we use the ApolloProvider component passing our client object and set our App component as the child component.

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import ApolloClient from "apollo-boost";
import { ApolloProvider } from "react-apollo";

const client = new ApolloClient({
  uri: "https://api.github.com/graphql",
  headers: {
    Authorization: "Bearer <Access Tokken>"
  }
});

ReactDOM.render(
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>,
  document.getElementById("root")
);

Trending Repositories GraphQL Query

We use the GitHub GraphQL Query.search resolver to query the repositories, we will provide query and cursor dynamic variables that we will pass to our query. Notice that the cursor variable is an optional because in our initial fetch we don’t have the end cursor to pass, so it just get the data from the start.

We also set the first parameter to get 15 repos per fetch and the search type as repository. Our return data consist of pageInfo containing the data for the endCursor that we will use in our subsequent fetch. The edges containing the node will provide all the repository data we will use to display in our UI.

import gql from "graphql-tag";

export const trendingRepositoriesGQLQuery = gql`
  query search($query: String!, $cursor: String) {
    search(first: 15, query: $query, type: REPOSITORY, after: $cursor) {
      pageInfo {
        startCursor
        endCursor
        hasNextPage
        hasPreviousPage
      }
      edges {
        node {
          ... on Repository {
            name
            owner {
              login
            }
            description
            stargazers {
              totalCount
            }
            primaryLanguage {
              name
            }
          }
        }
      }
    }
  }
`;

Infinite Scroll Repos Component

The Repos component responsibilities is to display our fetched repositories and provide the infinite scroll mechanism to fetch more data when the user scrolls to the bottom of the page. In our componentDidMount we add event listener for window on scroll event passing the handleOnScroll method, and in componentWillUnmount we remove the listener to avoid memory leak.

This component will receive the entries, loading, and onLoadMore function as the props. In the render method, we map the entries edges containing the node the repository and displays the name, description, stargazers count in our list. We also add simple loading text when the loading state is true, which means Apollo Client is currently fetching data from the network.

Inside handleOnScroll method we check if the scroll position of the page is at the bottom of the window, if yes we call the onLoadMore function from our props triggering subsequent fetch to the network. See more below at the integration with Query component part to see how we handle onFetchMore and update our query to get the repos after the next cursor.

import React, { Component } from "react";

class Repos extends Component {
  componentDidMount() {
    window.addEventListener("scroll", this.handleOnScroll);
  }

  componentWillUnmount() {
    window.removeEventListener("scroll", this.handleOnScroll);
  }

  handleOnScroll = () => {
    // http://stackoverflow.com/questions/9439725/javascript-how-to-detect-if-browser-window-is-scrolled-to-bottom
    var scrollTop =
      (document.documentElement && document.documentElement.scrollTop) ||
      document.body.scrollTop;
    var scrollHeight =
      (document.documentElement && document.documentElement.scrollHeight) ||
      document.body.scrollHeight;
    var clientHeight =
      document.documentElement.clientHeight || window.innerHeight;
    var scrolledToBottom = Math.ceil(scrollTop + clientHeight) >= scrollHeight;
    if (scrolledToBottom) {
      this.props.onLoadMore();
    }
  };

  render() {
    if (!this.props.entries && this.props.loading) return <p>Loading....</p>;
    const repos = this.props.entries.edges || [];
    return (
      <ul>
        {repos.map(({ node }, idx) => (
          <li key={idx}>
            <h3>
              {node.name} - {node.owner.login}
            </h3>
            <p>{node.description}</p>
            <p>
              ★ {node.stargazers.totalCount} -{" "}
              {node.primaryLanguage && node.primaryLanguage.name}{" "}
            </p>
          </li>
        ))}
        {this.props.loading && <h2>Loading...</h2>}
      </ul>
    );
  }
}

export default Repos;

Apollo Query Component Pagination Integration With Repos Component

The App component responsibilities are to provide encapsulation to Apollo Client Query component using the trendingRepositoriesGQLquery and passing the result, onFetchMore function that will trigger subsequent repos fetch to the Repos component as the child. It also updates the query to get the data after the initial pageInfo endCursor as well as merging the new repositories fetch with the previous repositories.

In our App component render method, we instantiate current date and use moment.js to subtract the week by one, then format the date to YYYY-MM-DD that will be used to construct the query string. The query string use the GitHub query syntax to get the created repositories later than the passed date and to be sorted ascending by the most stars count.

We use Apollo Client Query Component passing our trendingRepositoriesGQLquery as the query and passing our querystring as the dynamic variable to be use by the query. The Query Component uses the render prop pattern to share the GraphQL data with the UI. It also automatically tracks the loading and error state for us. Inside the Query Component we need to return the component that will be rendered inside a function that contains an object parameter with data loading, error, onFetchMore state and function.

We check if the error is not null, in case of an error occurs, and then just returned the message of the error to render. If there is no error we return the Repos component passing the data.search fetch result, loading state, and onLoadMore function to the Repos component.

The key on how the pagination will work is inside the onLoadMore function, inside it we use the Apollo Client onFetchMore passing the updated dynamic variable to query, this time we provide the cursor value which is the previous pageInfo endCursor so we can fetch the repositories after the last repo object. PageInfo is a Relay style cursor based pagination mechanism that contains endCursor for the next repositories to fetch.

The updateQuery function provide the mechanism to update the pageInfo end cursor and merge the data from previous result to the ReposComponent. Inside the function we will be passed the prevResult, and fetchMoreResult object. We can get the new edges and pageInfo from the fetchMoreResult, then if the new edges is not empty, which means the data is available we update our search data edges by merging the edges from prevResult and newEdges, we also update the pageInfo data to use the pageInfo from fetchMore result. If there is no new data we will just return the prevResult, this means there are no more endCursor available or we are at the end of the page results.

import React, { Component } from "react";
import { Query } from "react-apollo";
import "./App.css";
import Repos from "./components/repos";
import { trendingRepositoriesGQLQuery } from "./data/query";
import moment from "moment";

class App extends Component {
  render() {
    const date = new moment(new Date()).subtract(1, "weeks");
    const formattedDate = date.format("YYYY-MM-DD");
    const query = `created:>${formattedDate} sort:stars-desc`;
    return (
      <div>
        <h1>Last Week Trending Repositories</h1>
        <Query
          notifyOnNetworkStatusChange={true}
          query={trendingRepositoriesGQLQuery}
          variables={{
            query
          }}
        >
          {({ data, loading, error, fetchMore }) => {
            if (error) return <p>{error.message}</p>;
            const search = data.search;

            return (
              <Repos
                loading={loading}
                entries={search}
                onLoadMore={() =>
                  fetchMore({
                    variables: {
                      query,
                      cursor: search.pageInfo.endCursor
                    },
                    updateQuery: (prevResult, { fetchMoreResult }) => {
                      const newEdges = fetchMoreResult.search.edges;
                      const pageInfo = fetchMoreResult.search.pageInfo;
                      return newEdges.length
                        ? {
                            search: {
                              __typename: prevResult.search.__typename,
                              edges: [...prevResult.search.edges, ...newEdges],
                              pageInfo
                            }
                          }
                        : prevResult;
                    }
                  })
                }
              />
            );
          }}
        </Query>
      </div>
    );
  }
}

export default App;

Conclusion

It’s a wrap! We know that building a query and pagination mechanism ourselves is not easy and very error prone. Luckily we can use React Apollo Client Query Pagination mechanism that automatically track the loading, error, and onFetchMore results for us automatically. We just need to provide the updateQuery and merge the results, that’s it.

Apollo Client also provides built in caching mechanism and persistence for us so we can improve our app efficiency when loading data repeatedly. Although this article uses Relay style pageInfo cursor based pagination, we still can use offset based and other cursor based pagination using the ApolloClient onFetchMore mechanism.

To see all the source for this project you can visit and clone the repository in the Github.