Pagination In React Apollo Client
React
GraphQL
15/12/2022
Continuing with the theme of fetching hundreds of events: In this post, you'll learn how to implement pagination on the client-side with Apollo client and infinite scrolling.
This example describes the implementation details of cursor-based pagination on the client side. For more information on the database and GraphQL implementation, check out the previous pagination post.
Query document
While the events
query is defined in a separate GraphQL schema file, we need a query document to tell Apollo what event data we are requesting.
query events($filter: EventsInput!) { events(filter: $filter) { edges { id title # More fields, etc... } pageInfo { endCursor hasNextPage } }}
Initial query
For the initial request, we only need to pass in the limiter as a variable to the EventsDocument
query.
const { data, // Needed for paginating results later on. fetchMore, networkStatus,} = useQuery(EventsDocument, { variables: { input: { // `LIMIT` may represent any number. first: LIMIT, }, }, notifyOnNetworkStatusChange: true,})
Make note of notifyOnNetworkStatusChange
. By setting it to true
in the options parameter of useQuery
, we get more fine-grained information on the network status with networkStatus
. This is necessary to show the appropriate loading indicator, as we will make two different types of requests.
For the initial request, we would check for the network status loading
. However, later on, when we fetch more data using fetchMore
, we would check a status of fetchMore
.
if (networkStatus === NetworkStatus.loading || !data || !data.events) { return <SpinningCircle />}
Displaying data
Displaying the data is as simple as iterating it with the map
method. No magic here.
However, keep in mind we would like to implement infinite scrolling. This means when we approach the final event in the list, we need to fetch more events. How do we do this?
There's a great library called React Waypoint that allows us to trigger a function as soon as a Waypoint element (i.e. after the final event) enters the viewport.
<div> {data.events.edges.map((event, index, thisArray) => ( <div key={event.id}> <EventCard event={event} /> {/* Add a Waypoint element after the last element */} {index === thisArray.length - 1 && <Waypoint onEnter={fetchMoreEvents} />} </div> ))}
{networkStatus === NetworkStatus.fetchMore && <SpinningCircle />}</div>
When the Waypoint element enters the viewport, it triggers the fetchMoreEvents
function, which calls the fetchMore
function from useQuery
.
Notice how we display a loading spinner when we fetch more data and check the network for a fetchMore
status.
Fetching paginated data
After fetchMoreEvents
is triggered, we call fetchMore
only once we've confirmed that there is actually more data to be found using hasNextPage
. Once we fetch, we pass in the cursor information in addition to the limiter.
const { events: { edges, pageInfo },} = data
const fetchMoreEvents = () => { if (!pageInfo.hasNextPage) { return }
fetchMore({ variables: { first: LIMIT, cursor: pageInfo.endCursor, }, updateQuery: (prevResult, { fetchMoreResult }) => { fetchMoreResult.events.edges = [ ...prevResult.events.edges, ...fetchMoreResult.events.edges, ]
return fetchMoreResult }, })}
Pay special attention to updateQuery
. ⚠ By default, if we query new data, our existing events will simply be overwritten by the new data. We do not want this!
Instead, we want to merge the our existing data into to newly fetched set of data. In the function, we are merging our existing results prevResults.events.edges
into our new results fetchMoreResult.events.edges
, which we then return.