Apollo Client + Relay-style cursor pagination

Howdy! I’m relatively new to GraphQL & Apollo and I’ve been stuck the past few days trying to figure out how to get caching to work with relay-style pagination.
(My first post got eaten so fingers crossed!)

I’ve included a gif below. You can see the updated (merged, not overwritten) and yet it doesn’t appear that we’re reading from the cache.

Pagin Issue

Code

The code is very basic. I’m hoping to get this working as I expect before building it out.

In my main script:

import React from "react";
import ReactDOM from "react-dom";
import PostFeed from "../_components/feeds/PostFeed";

document.addEventListener( "DOMContentLoaded", () => {
	const renderedFeed = document.querySelector( '.post-feed-app' );

	ReactDOM.render(
		<PostFeed />,
		renderedFeed
	);
} ); // End on DOMContentLoaded.

Query

/*--------------------------------------------------------------
# Define Main Query
--------------------------------------------------------------*/
const GET_POSTS_QUERY = gql`
	query MorePosts(
		$first: Int
		$last: Int
		$after: String
		$before: String
	) {
		posts(
			first: $first,
			last: $last,
			after: $after,
			before: $before
		) {
			edges {
				node {
					id
					databaseId
					title
				}
			}
			pageInfo {
				hasNextPage
				hasPreviousPage
				startCursor
				endCursor
			}
		}
	}
`;

Components

import {
	ApolloClient,
	ApolloProvider,
	InMemoryCache,
	useQuery,
	gql
} from "@apollo/client";

import { relayStylePagination } from "@apollo/client/utilities";

const perPage = 5;

/*--------------------------------------------------------------
# Define Client
--------------------------------------------------------------*/
const relayCacheOptions = {
	typePolicies: {
		Query: {
			fields: {
				posts: relayStylePagination(),
			}
		}
	}
};

const client = new ApolloClient( {
	uri: 'https://testingblocks.local/graphql', /* local path */
	cache: new InMemoryCache( relayCacheOptions )
} );

/*--------------------------------------------------------------
# Define Main Query
--------------------------------------------------------------*/
const GET_POSTS_QUERY = gql`
	# See above
`
/*--------------------------------------------------------------
# Define `updateQuery` - update the query with the new results
--------------------------------------------------------------*/
const updateQuery = (previousResult, { fetchMoreResult }) => {
	return fetchMoreResult.posts.edges.length ? fetchMoreResult : previousResult;
};

/*--------------------------------------------------------------
# Define Postlist - Component that shows the paginated list of posts
--------------------------------------------------------------*/
const PostList = ({ data, fetchMore }) => {
	const { posts } = data;
	return (
	  <div>
		{posts && posts.edges ? (
		  <div>
			<ul>
			  {posts.edges.map(edge => {
				const { node } = edge;
				return (
					<li	key={node.id}>
						{node.title}
					</li>
				);
			  })}
			</ul>
			<div>
			  {posts.pageInfo.hasPreviousPage ? (
				<button
					onClick={() => {
						fetchMore({
							variables: {
								first: null,
								after: null,
								last: perPage,
								before: posts.pageInfo.startCursor || null
							},
							updateQuery
						});
					}}
				>
				  Previous
				</button>
			  ) : null}
			  {posts.pageInfo.hasNextPage ? (
				<button
					onClick={() => {
						fetchMore({
							variables: {
								first: perPage,
								after: posts.pageInfo.endCursor || null,
								last: null,
								before: null
							},
							updateQuery
						});
					}}
				>
				  Next
				</button>
			  ) : null}
			</div>
		  </div>
		) : (
		  <div>No posts were found...</div>
		)}
	  </div>
	);
};

/*--------------------------------------------------------------
# Define Posts
--------------------------------------------------------------*/
function Posts() {
	const variables = {
		first: perPage,
		last: null,
		after: null,
		before: null
	};

	const { data, error, loading, fetchMore } = useQuery(
		GET_POSTS_QUERY, {
			variables,
			notifyOnNetworkStatusChange: true
		}
	);

	if (error) {
		return <pre>{JSON.stringify(error)}</pre>;
	}

	if (loading) {
		return <p><i>Loading...</i></p>;
	}

	console.log({data});

	return (
		<PostList
			data={data}
			fetchMore={fetchMore}
		/>
	);
};

/*--------------------------------------------------------------
# Final component
--------------------------------------------------------------*/
export default function PostFeed() {
	console.log({client})
	return (
		<ApolloProvider client={client}>
			<div>
				<h2>Post List</h2>
				<Posts />
			</div>
		</ApolloProvider>
	);
}

Thank you for taking the time to read this! I’m hoping it’s something obvious. Please let me know if you have any questions, I’m happy to provide more context. Thanks again! :slight_smile:

References used:

https://www.wpgraphql.com/2020/03/26/forward-and-backward-pagination-with-wpgraphql

Hi! Definitely understand the confusion. I’ll try to clear things up as much as I can.
First of all, the view of the cache inside that gif isn’t quite what you think it is. That top level CACHE view shows all entities that apollo client has ever received, whether or not they are currently part of a query’s result set. You can see the query result set in that ROOT_QUERY entity at the top of the list.

The next thing is about how pagination works with the apollo cache. When you use a pagination policy like relayStylePagination(), the query results are returned to you as one large array containing all of the results that have been loaded so far. This is all done through the type policy, and updateQuery should not be necessary. updateQuery was the main way to do pagination in apollo-client v2, but with the introduction of type policies in v3, it is generally not recommended to use updateQuery for pagination anymore. So updateQuery can be removed here.

So now useQuery should be returning a single array containing all results fetched so far, all merged together in a single result set. If your goal is to display a limited set of results that can be traversed with “Previous” and “Next” buttons, you will need to split up the results into individual pages yourself. You can keep track of which page is displayed using local react state (useState). The “Previous” and “Next” buttons can then update this local state to change which page is displayed. The buttons should not be calling fetchMore on every click; you only want to call fetchMore when the results you want to display haven’t been loaded yet. Doing it this way has benefits, such as being able to change the number of displayed results without refetching anything.

Hope this helps! Let me know if you have any more questions

2 Likes