Apollo comes with a simple method fetchMore()
that you can use to implement offset pagination with infinite scroll in your React application.
Offset pagination is very popular in modern applications due to its convenience: The 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 with Apollo and apollo-link-state
.
So how such pagination is done? 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 quantity. And to implement offset pagination, we use these technologies:
- Apollo Client to use GraphQL in React
react-apollo
to use custom React componentsQuery
andApolloProvider
apollo-link-state
, a data provider and state manager from Apollo
Using the listed technologies, we can create necessary GraphQL resolvers, React components, and an instance of Apollo Client instance in order to implement pagination:
- An instance of Apollo Client with configured
apollo-link-state
- GraphQL resolvers to be passed to
apollo-link-state
to retrieve the data - A React 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 React application will retrieve chapters from the famous Harry Potter and Hobbit series of books and render them. This is how the end result looks:
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
The Apollo stack comes with apollo-link-state
, a great package that helps to manage the state for our React apps and is used instead of Redux in applications that use GraphQL. Since we don’t have a backend API for our demo app, we use apollo-link-state
.
Here’s App.js
with configured Apollo Client and a basic component:
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;
Code language: JavaScript (javascript)
As you can see, apollo-link-state
configurations are set in the clientState
property, which points to the resolvers that retrieve items from the backend API or, in this app, from apollo-link-state
.
Notice that ApolloProvider
renders the ChapterListQuery
component, which will be sending GraphQL queries. ChapterListQuery
also returns a dumb component ChapterList
to render chapters.
We’re now going to create resolvers, and then we’ll switch to the React components.
Creating GraphQL resolvers for apollo-link-state
for offset pagination
A minimal configuration object for apollo-link-state
should have two properties: defaults
to return some data by default and resolvers
to return new data.
Have a look at the code below:
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)
}
}
};
Code language: JavaScript (javascript)
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 object from mockChapters
is transformed to a new object with this property.
To make sure that our React application renders the first ten chapters once you open it in the browser, we set the defaults.chapters
property to a getChapters({})
call.
Finally, we set the 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 always 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 rendered list.
Creating a React component with a GraphQL query
Since it’s a bad idea to create components that both render data and send GraphQL queries to the backend, we created ChapterListQuery
, a Higher Order Component that’s only concerned with querying the backend. Then we have another React component, ChapterList
, used only for rendering. We’ll look at ChapterList
in the next section.
Here’s ChapterListQuery
:
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;
Code language: JavaScript (javascript)
There are several key aspects that you need to pay attention to in the code snippet above:
GET_CHAPTERS
query with offset
and limit
Here’s the GraphQL query:
const GET_CHAPTERS = gql`
query chapters($offset: String) {
chapters(limit: 10, offset: $offset) @client {
id
title
}
}
`;
Code language: PHP (php)
GET_CHAPTERS
is a typical GraphQL query that uses the gql
method from Apollo. Notice that we specified these three values:
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 forapollo-link-state
. It tells Apollo that the data must be resolved from the cache and that making a network request isn’t necessary.
This GraphQL 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()
:
const ChapterListQuery = () => (
<Query query={GET_CHAPTERS}>
{({ data, fetchMore }) =>
data && (
<ChapterList
chapters={data.chapters || []}
onLoadMore={() => fetchMore(//...)} />
)
}
</Query>
);
Code language: JavaScript (javascript)
What’s important to successfully implement offset-based pagination is to add the onLoadMore()
function, which is shown below:
onLoadMore={() =>
fetchMore({
variables: {
offset: data.chapters.length
},
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) return prev;
return Object.assign({}, prev, {
chapters: [...prev.chapters, ...fetchMoreResult.chapters]
});
}
})
}
Code language: JavaScript (javascript)
As you can see, onLoadMore()
calls the Apollo 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 array. We pass only an offset
value, which always equals the length of the current array. You can also pass limit if it’s necessary for your React app.
The other property inside fetchMore()
is updateQuery()
, a function that accepts two parameters — prev
and a {fetchMoreResult}
object. prev
contains the data returned by the previous query, and {fetchMoreResult}
stores new data.
Notice how updateQuery()
changes the result of the current query on the fly via Object.assign()
. Why is it necessary? Consider this: We need each GraphQL query to return only a part of items array for offset-based pagination. Our React app, however, displays not only the items just returned, but all the items including the ones loaded before. Hence, we must merge the current array loaded to our React application with its next part in updateQuery()
.
You can look at it as you first cut the original array mockChapters
in parts and then re-create it chunk by chunk in the React app.
That’s it for ChapterListQuery
, and we can review the ChapterList
component.
Creating the ChapterList
component with infinite scroll
We need a component to display the list of chapters with a scroll bar. This component, ChapterList
, receives items and the onLoadMore()
function to load more data. And whenever we scroll to the bottom of the list, onLoadMore()
must be called.
Here’s the ChapterList
implementation:
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;
Code language: JavaScript (javascript)
First, we created a function handleScroll()
that gets called whenever the scroll
event happens. handleScroll()
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 other items, a GraphQL query is sent.
The entire flow in our React application with pagination created with Apollo is this:
ChapterList
gets rendered and runsonLoadMore()
when the list is scrolled to the bottom.ChapterListQuery
sends a GraphQL query, which gets resolved byapollo-link-state
(no HTTP request is actually sent).apollo-link-state
uses resolvers to slice a chunk of requested items and respond to the GraphQL query.- New chapters are returned and
updateQuery()
infetchMore()
gets called to add the latest chapters. - React re-renders the application to show new items.
That’s how you implement offset-based pagination with infinite scroll in React using Apollo and apollo-link-state
.