Configure Apollo Client InMemoryCache Field Policies for Paginated Queries
Learn how to set Apollo Client InMemoryCache field policies for offset/limit and cursor-based pagination so fetchMore merges pages correctly without duplicates.
11 Oct 2026, 08:29 UTC

Desired Outcome
Configure Apollo Client's InMemoryCache with field policies so that fetchMore results for paginated lists merge into a single cached list without duplicates or gaps. This applies to both offset/limit and cursor‑based (Relay) pagination.
Prerequisites
- Apollo Client 3.x (
@apollo/client) installed - A GraphQL schema exposing paginated fields — either
offset/limitarguments or Relay cursor connections (after/first,before/last) - An existing
InMemoryCacheinstance in yourApolloClientsetup
Procedure
1. Define a TypePolicy for the paginated field
When creating the InMemoryCache, add a typePolicies entry that targets the type owning the field (usually Query for root fields or the parent object type for nested fields).
import { ApolloClient, InMemoryCache } from '@apollo/client';
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
// Offset/limit example
products: {
keyArgs: ['offset', 'limit', 'filter'],
merge(existing = [], incoming, { args }) {
if (args?.offset === 0) {
return incoming; // first page replaces
}
return [...existing, ...incoming]; // append subsequent pages
},
},
// Cursor‑based (Relay) example
users: {
keyArgs: ['after', 'first', 'filter'],
merge(existing, incoming, { args }) {
if (!existing) return incoming;
if (args?.after == null) return incoming; // first page (no cursor)
return {
...incoming,
edges: [...existing.edges, ...incoming.edges],
};
},
},
},
},
},
});
const client = new ApolloClient({ cache });
2. Understand keyArgs behavior
keyArgs tells the cache which argument combinations should create separate entries. Include every pagination argument that should not split the cache:
- Offset/limit:
['offset', 'limit', 'filter']– each unique filter gets its own list; offset/limit variations merge into that list - Cursor‑based:
['after', 'first', 'filter']– same principle - Omitting
keyArgsmerges all argument combinations into one list, breaking pagination - Setting
keyArgs: falsecreates a separate entry per argument set, preventing merge
3. Write the merge function
The merge function receives (existing, incoming, { args, isReference, toReference }). Handle the first page and subsequent pages:
- First page:
existingisundefinedor args indicate first page (offset === 0 or no cursor). Returnincomingdirectly. - Subsequent pages: For offset/limit, concatenate arrays. For cursor connections, append
edgeswhile preservingpageInfofromincoming(it reflects the latest page). - Non‑array fields: If the field returns an object with
edges/nodesplustotalCount, merge the array portion and keep other fields fromincoming.
4. Optional: Add a read function
A read function lets you return a slice of the cached list without a network request when the requested range is already present.
products: {
keyArgs: ['offset', 'limit', 'filter'],
merge(existing = [], incoming, { args }) {
if (args?.offset === 0) return incoming;
return [...existing, ...incoming];
},
read(existing, { args, toReference }) {
if (!existing) return existing;
const offset = args?.offset ?? 0;
const limit = args?.limit ?? existing.length;
return existing.slice(offset, offset + limit);
},
}
Expected Checks
- Cache inspection: Open Apollo DevTools → Cache tab. Verify a single entry for the field (e.g.,
Query.products) containing all fetched items in order. - No duplicate
__refentries: Each item should appear once. Duplicates indicate incorrectkeyArgsor a merge function that doesn't deduplicate. - Network behavior: Only the first page and subsequent
fetchMorecalls fire network requests. Re‑reading the same page hits the cache. - Query consistency: A query with the original arguments returns the full merged list (or the sliced portion if a read function is defined).
Recovery Options
Evict and refetch a single field
// In a component or after a mutation
cache.evict({ fieldName: 'products' });
// Or with specific arguments
cache.evict({ id: 'ROOT_QUERY', fieldName: 'products', args: { filter: 'active' } });
// Then trigger a refetch
refetch(); // from useQuery
Reset entire cache
cache.reset(); // clears all normalized data
// Or create a fresh client (useful on logout)
const newClient = new ApolloClient({ cache: new InMemoryCache({ /* same policies */ }) });
Iterate on the policy
- Add missing filter arguments to
keyArgs - Handle
existing === undefinedin the merge function - Preserve
pageInfofromincomingfor cursor pagination
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.