'How to implement infinite scroll with GraphQL and React' post illustration

How to implement infinite scroll with GraphQL and React

avatar

13 Feb,

2019

Apollo comes with a simple method fetchMore() that you can use to implement offset pagination with infinite scroll. This kind of pagination is very popular in modern applications due to its convenience: Data is presented as an endless stream, and you can infinitely scroll without reaching the end (in theory). In this tutorial, we explain how such pagination can be implemented in a React application using Apollo and apollo-link-state.

So what needs to be done to achieve the desired effect? The next short section summarizes what you need to do.

How to implement offset-based pagination with Apollo and React

First, we'd like to remind what the offset-based pagination is. With this kind of pagination, your client application must provide an offset and limit values when querying the server for new items to indicate what entries must be returned and also their amount. And to implement offset pagination, we use these technologies:

  • Apollo Client to use Apollo in React.
  • react-apollo to use custom React components Query and ApolloProvider.
  • apollo-link-state, a data provider and state manager. You can replace it with a GraphQL server in a real-world application.

Using the listed technologies, we can create resolvers, components, and Apollo Client instance and implement pagination.

In this tutorial, we create these components:

  • An instance of Apollo Client with configured apollo-link-state
  • GraphQL resolvers to be passed to apollo-link-state to retrieve the data
  • A component that renders the data and asks for new entries when a condition is met
  • A Higher Order Component, also known as a provider, which sends GraphQL queries for new data

Our example application will retrieve chapters from the famous Harry Potter and Hobbit series of books and render them. This is how the end result looks:

Simple React application with offset-based pagination with infinite scroll created with Apollo and Apollo Link State

If you want to look at the code with our pagination example, check out these two links:

Let's now focus on the implementation.

Configuring apollo-link-state

Apollo comes with apollo-link-state, a great package that helps to manage the application state and is used instead of Redux. Since we don't have a backend API for this applciation, we use apollo-link-state to get the chapters to render.

Here's App.js with configured Apollo Client and a basic component:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import React, { Component } from 'react';
import ApolloClient from 'apollo-boost';
import { ApolloProvider } from 'react-apollo';

import ChapterListQuery from '../chapters/ChapterListQuery';
import chapterResolvers from '../chapters/resolvers';

import './App.css';

const client = new ApolloClient({
  clientState: chapterResolvers
});

class App extends Component {
  render() {
    return (
      <ApolloProvider client={client}>
        <ChapterListQuery />
      </ApolloProvider>
    );
  }
}

export default App;

As you can see, apollo-link-state configurations are set in the clientState property, which points to the resolvers to retrieve data upon GraphQL requests, and so chapterResolvers are imported and used in the Apollo configuration object. We show the resolvers in the next section.

Concerning the layout, notice that ApolloProvider component renders the ChapterListQuery component, which will be sending GraphQL queries. It also returns a dumb component ChapterList that renders chapters.

First, we create resolvers, and then we switch to the components that query and render chapters.

Creating resolvers for apollo-link-state for offset pagination

apollo-link-state, as we've mentioned already, needs a configuration, and the minimal configuration object should have two properties: defaults to return some data by default and resolvers to return new data.

Have a look at the code below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import mockChapters from '../data/chapters';

const getChapters = ({ limit = 10, offset = 0 }) =>
  mockChapters
    .slice(offset, offset + limit)
    .map(chapter => ({ ...chapter, __typename: 'Chapter' }));

export default {
  defaults: {
    chapters: getChapters({})
  },
  resolvers: {
    Query: {
      chapters: (_, variables) => getChapters(variables)
    }
  }
};

First, we import mockChapters, an array of chapter objects. Second, we implement the getChapters() function, which uses the limit and offset values to slice a chunk of the array and return only the required part.

Pay attention that apollo-link-state requires that each object has the __typename property with some value (such as Chapter) for normalization, which is why each chapter object from mockChapters is transformed to a new object with this property.

To make sure that our application renders the first ten chapters once you open it in the browser, we set the defaults.chapters property to a getChapters() call.

Finally, in the resolvers.Query property we set a query chapters to a getChapters() call with variables. As required by the offset-based pagination implementation, you can pass both limit and offset variables into getChapters(). Although, in this implementation we only pass an offset and use a default limit.

Now we can focus on creating a component that sends a new GraphQL query each time you scroll to the end of the chapter list.

Creating a React component with a GraphQL query

Since it's a bad idea to create a component that both renders data and sends queries to the database, we created ChapterListQuery, a Higher Order Component that's only concerned with querying the backend for data. The other component, ChapterList, will render the chapters. We'll look at it in the next section.

Here's the ChapterListQuery code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import React from 'react';
import { Query } from 'react-apollo';
import gql from 'graphql-tag';

import ChapterList from './ChapterList';

const GET_CHAPTERS = gql`
  query chapters($offset: String) {
    chapters(limit: 10, offset: $offset) @client {
      id
      title
    }
  }
`;

const ChapterListQuery = () => (
  <Query query={GET_CHAPTERS}>
    {({ data, fetchMore }) =>
      data && (
        <ChapterList
          chapters={data.chapters || []}
          onLoadMore={() =>
            fetchMore({
              variables: {
                offset: data.chapters.length
              },
              updateQuery: (prev, { fetchMoreResult }) => {
                if (!fetchMoreResult) return prev;
                return Object.assign({}, prev, {
                  chapters: [...prev.chapters, ...fetchMoreResult.chapters]
                });
              }
            })
          }
        />
      )
    }
  </Query>
);

export default ChapterListQuery;

There are several key aspects that you need to pay attention to:

GET_CHAPTERS query with offset and limit

Here's the GraphQL query to be send to get new chapters:

1
2
3
4
5
6
7
8
const GET_CHAPTERS = gql`
  query chapters($offset: String) {
    chapters(limit: 10, offset: $offset) @client {
      id
      title
    }
  }
`;

GET_CHAPTERS is a typical query that uses Apollo's gql method to get new data. We specified these three values in the query:

  • limit, the number of items that the resolver must send in one go.
  • offset, a pointer to the item starting from which the resolver will retrieve data. Offset equals the length of the previous data.
  • @client, a directive needed only for apollo-link-state. It tells Apollo that the data must be resolved from the cache and making a network request isn't required.

This query will send the limit and offset values to the resolver so that the resolver understands how much data it should return.

The Query wrapper component with the onLoadMore() method

We use a Query component (kindly presented by react-apollo) to access data from our component and, importantly, to get the fetchMore() method to fetch new data when necessary.

ChapterListQuery returns a new component, ChapterList, and passes it two props attributes: chapters and onLoadMore():

1
2
3
4
5
6
7
8
9
10
11
const ChapterListQuery = () => (
  <Query query={GET_CHAPTERS}>
    {({ data, fetchMore }) =>
      data && (
        <ChapterList
          chapters={data.chapters || []}
          onLoadMore={() => fetchMore(//...)} />
      )
    }
  </Query>
);

What chapters are necessary for is self-evident. But what's important to successfully implement offset-based pagination is to add an onLoadMore() function.

Let's have a closer look at onLoadMore():

1
2
3
4
5
6
7
8
9
10
11
12
13
onLoadMore={() =>
  fetchMore({
    variables: {
      offset: data.chapters.length
    },
    updateQuery: (prev, { fetchMoreResult }) => {
      if (!fetchMoreResult) return prev;
      return Object.assign({}, prev, {
        chapters: [...prev.chapters, ...fetchMoreResult.chapters]
      });
    }
  })
}

As you can see, onLoadMore() calls the Apollo's fetchMore() method, which gets returned by a GraphQL query and is available thanks to apollo-link-state.

fetchMore() receives an object parameter with two properties. First, we must pass the variables that the resolver will use to slice the next part of the chapters array to return. We pass only an offset value, which always equals the length of the current chapters array, but you can also pass a limit value if it's necessary for your application.

There's another property inside fetchMore()updateQuery(). This is a function that accepts two parameters. One parameter is the data returned by the previous query, and the other parameter is the new data stored in the {fetchMoreResult} object.

Basically, updateQuery() is necessary to change the result of the current query on the fly. Consider this: each GraphQL query will only return a part of the chapters array, whilst you need to display not only the chapters returned now, but all the chapters including the ones loaded earlier. Hence, you need to merge the current chapter array in the React application with its next part.

You can look at it as if you first cut the original array mockChapters in parts and then re-create it chunk by chunk on the client.

That's it for the ChapterListQuery component, and we can review the ChapterList component.

Creating the ChapterList component with infinite scroll

We need a component to display a list of chapters with a scroll bar. This component, ChapterList, receives chapters and the onLoadMore() function for loading more data. And whenever we scroll to the bottom of the list, the onLoadMore function gets called.

Here's the ChapterList implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import React from 'react';

const handleScroll = ({ currentTarget }, onLoadMore) => {
  if (
    currentTarget.scrollTop + currentTarget.clientHeight >=
    currentTarget.scrollHeight
  ) {
    onLoadMore();
  }
};

const ChapterList = ({ chapters, onLoadMore }) => (
  <div>
    <h2>Chapters</h2>
    <ul className="list-group chapters-list"
        onScroll={e => handleScroll(e, onLoadMore)}>
      {chapters.map(({ id, title }) => (
        <li key={id} className="list-group-item">
          {title}
        </li>
      ))}
    </ul>
  </div>
);

export default ChapterList;

First, we created a function handleScroll() that gets called whenever the scroll event happens and executes the onLoadMore() function. handleScroll() also calculates whether it's time to load new chapters by simply analyzing if you scrolled until the last item in the list. And if you did, onLoadMore() gets called and, provided there are more chapters, a GraphQL query is called, new chapters are rendered.

The entire flow is this:

  1. ChapterList gets rendered and runs the onLoadMore() method provided by ChapterListQuery HOC.
  2. ChapterListQuery sends a GraphQL query, which gets resolved by apollo-link-state (no HTTP request is sent).
  3. apollo-link-state uses resolvers to slice a chunk fo chapters using the passed offset and limit values and respond to the query chapters.
  4. New chapters are returned, and the updateQuery method in fetchMore() gets called to add the latest chapters to the array of already loaded chapters.
  5. React re-renders the application by adding new chapters.

That's how you implement offset-based pagination with infinite scroll in React using Apollo and apollo-link-state.

Comments