Apollo Client 3 cache management of cursor-paginated lists

These are notes and findings related to building the Apollo Client cache for cursor-based pagination in practice app I am building, blog-apollo-app, wherein I am striving to find optimal solutions for various challenges.

Findings

Schema

The app implements cursor-based pagination, which looks like:
# INPUTS enum SortDirection { asc desc } input CursorPaginationInput { field: String sortDirection: SortDirection cursor: String limit: Int } input GetPostsParams { pagination: CursorPaginationInput! ownerId: String tagId: String } # OUTPUTS type Cursors { start: String end: String next: String } type CursorPaginatedPosts { items: [Post!]! cursors: Cursors! }
 

Merging successive pages

This structure is a bit different from the out-of-the-box offset-based pagination provided by Apollo. However, the offset-based pagination typePolicies configuration can be closely followed to manage the cursor pagination cache. The app’s cache ends up looking pretty much the same:
export const apolloCache = new InMemoryCache({ typePolicies: { Query: { fields: { getPosts: cursorPaginatedField() }, }, } }); function cursorPaginatedField(): FieldPolicy { return { keyArgs: false, merge(existing, incoming) { return { ...incoming, items: [...(existing?.items || []), ...(incoming?.items || [])], }; } }; }
The merge logic enables correct fetchMore query functionality. When additional items are fetched, they get appended to the items array. The ...incoming spread dumps the incoming cursor metadata so that each successive call gets the next page.

Merging mutation results

If a paginated list needs to update according to a mutation, the cache update can be managed via refetching or manual cache management. I encountered unexpected behavior with refetching, so I used the mutation’s update and cache.modify:
const [createPost, postCreation] = useCreatePostMutation({ update: (cache, { data }) => { cache.modify({ fields: { getPosts(existing: generated.GetPostsQuery['getPosts'], { toReference }) { if (data) { const newPost = toReference({ __typename: data.createPost.__typename, id: data.createPost.id }); return { cursors: existing.cursors, items: [newPost, ...existing.items], }; } } } }); } });
It is important to ensure the mutation’s returned fields contains at least all of the paginated query’s fields. Otherwise, a created resource will not be cached as part of the query, and the query will require a network refetch to reflect the latest state.
 

Still under investigation

In server-side queries, the client cache needs to be cleared to avoid stale data. In my case, without this cache-clear, subsequent page refreshes to not furnish fresh data from new mutations. The below workaround seems to work for now, but I’m not sure why:
export async function getServerSideProps(context) { const client = initializeApollo() // ensure non-stale cache await client.resetStore(); await client.clearStore();
Relevant links:

Unanswered

According to the Apollo docs, mutations can trigger cache updates via refetchQueries (https://www.apollographql.com/docs/react/data/mutations/#refetching-queries).
It states:
Each included query is executed with its most recently provided set of variables
This is necessary for a paginated-fetch to send the most recent cursors. Unfortunately, in the app, the refetch was sent with the initial variables.
Relevant links: