Apollo Client fetchMore Duplicates or Resets: A Cache Merge Diagnostic Guide
fetchMore can return the correct page while the normalized cache renders duplicates or snaps back to page one. Here is how to tell which cache setting is responsible.
26 Aug 2026, 12:55 UTC

You click Load more, the network tab shows a correct second page, and the list still breaks: rows appear twice, the view snaps back to page one, or earlier items vanish after you navigate away and return. In Apollo Client 3.x, that combination usually points at the normalized cache rather than the request. This guide separates a cache merge problem from a server paging problem and ties each fix to the finding that justifies it.
Short version: a paginated field is stored as one cache entry unless you tell Apollo otherwise. Without a field policy, each fetchMore result replaces the stored list. The fix is a typePolicies entry with the right keyArgs and a merge function, but the right shape depends on whether you paginate by offset or by cursor.
The symptom is cache-side when the page itself is correct
Before changing configuration, confirm the raw response. Log the variables passed to fetchMore and inspect the response body in the Network panel or your client's devtools. If page two contains the expected rows and the UI still duplicates or resets, the cache is the prime suspect. If the response itself repeats rows or returns an empty page, stop and treat it as a server or resolver issue.
Diagnostic table: match the symptom to the likely cause
Symptom after fetchMore | Likely cache cause | First check |
|---|---|---|
| List resets to the first page | keyArgs was not disabled for offset pagination, so page-two variables create a different field entry | Inspect the cache entry and its key arguments in Apollo devtools |
| Duplicate rows | The merge concatenates without deduplicating while the server returns overlapping pages | Compare ids across page one and page two in the raw responses |
| Items missing or cache appears empty | Missing __typename or non-unique ids cause normalization collisions | Check returned objects for __typename and stable ids |
| Earlier pages lost after remount or back-navigation | Merge writes only the incoming page, or a persisted cache restores a partial entry | Test with a full page reload versus in-app navigation |
Ordered checks
- Reproduce with a minimal query and log the
fetchMorevariables plus the raw server response. Confirm the requested page is correct before touching cache configuration. - Open Apollo devtools and inspect the normalized cache entry for the paginated field. Note whether page two created a separate entry keyed by its arguments.
- Read the
typePoliciesentry for that field. VerifykeyArgsand themergefunction against the pagination style you actually use. - Check every returned object for
__typenameand a unique id. Normalization depends on both. - Switch the query to
network-onlyand clear the cache. If the symptom disappears, the cache is confirmed as the source. - Navigate away and back, then do a full page reload. This distinguishes in-memory cache behavior from persisted or restored cache behavior.
Fixes tied to each finding
Offset pagination: disable keyArgs and concatenate
If page-two variables create a separate cache entry, set keyArgs: false for that field so all argument sets share one entry, and add a merge that places incoming items at their offset. This is the pattern Apollo documents for offset pagination; adjust the field name and argument names to match your schema.
Cursor connections: merge edges and decide pageInfo
Relay-style connections keyed by cursor arguments become separate entries unless keyArgs is false or a stable subset. The merge should concatenate edges and carry pageInfo forward. You must decide explicitly how startCursor and endCursor are preserved; copying only the incoming pageInfo can make later pagination behave as if earlier pages never existed.
Duplicates: deduplicate or fix overlapping pages
If the merge concatenates and the server returns overlapping pages, deduplicate on a stable id plus __typename. If pages genuinely overlap because of a server-side cursor or offset bug, fix the server logic instead of hiding it in the client merge.
Missing items or empty cache: check __typename and ids
Missing __typename or non-unique ids cause normalization collisions. Return both from the server, or use a merge that does not depend on normalized identity for that field. Do not assume the cache is always at fault; unstable ids are a server or schema concern.
Example: offset pagination field policy
The following configuration illustrates the shape for an offset-paginated list. It has not been executed here; replace items and offset with your actual field and argument names.
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
items: {
keyArgs: false,
merge(existing, incoming, { args }) {
const offset = args?.offset ?? 0;
const merged = existing ? existing.slice(0) : [];
for (let i = 0; i < incoming.length; i++) {
merged[offset + i] = incoming[i];
}
return merged;
},
},
},
},
},
});
For a cursor connection, the merge is different in kind:
feed: {
keyArgs: false,
merge(existing, incoming) {
const edges = existing ? [...existing.edges, ...incoming.edges] : incoming.edges;
return {
...incoming,
edges,
pageInfo: {
...incoming.pageInfo,
startCursor: existing?.pageInfo?.startCursor ?? incoming.pageInfo?.startCursor,
},
};
},
},
This assumes edges are unique. If the server can return overlapping edges, deduplicate before returning the merged object.
Verify the fix
- Write a small unit test that calls the merge function with existing and incoming page objects and asserts the resulting array length, order, and absence of duplicates. Run it with your project's test runner, for example
npm test. - Compare
cache-and-networkagainstnetwork-onlyfor the same interaction. A correct merge should render the same list in both cases. - Check the installed Apollo Client version and its migration or changelog notes for
typePolicies,keyArgs, andupdateQuerybehavior. - Test navigation away and back, plus a full page reload, to distinguish in-memory cache behavior from persisted or restored cache behavior.
When to escalate to backend or schema owners
- Server cursors or offsets overlap, so pages genuinely repeat or skip rows.
- Object ids are unstable across requests, making normalization unreliable.
- The schema lacks a connection type or a stable pagination contract for the list.
- The raw response is wrong before the cache is involved.
Limitations and version assumptions
typePolicies, keyArgs, and field policies are Apollo Client 3.x concepts. Older 2.x code relied on updateQuery, and newer major versions may change defaults, so confirm the installed client version before applying fixes. keyArgs: false is broad: if the same field is queried with genuinely different filters, unrelated lists can be merged, so consider keying on filter arguments instead. Merge functions run on every write, and a non-idempotent merge can duplicate data on refetch, cache restore, or optimistic updates. Treat remembered API signatures as unverified and check the documentation for the version you actually have installed.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.