InMemoryCache typePolicies for Array Merge Strategies
0 reputation · 15 Dec 2023, 23:10 UTC
0 reputation · 15 Dec 2023, 23:10 UTC
Apollo Client's InMemoryCache utilizes typePolicies to define how incoming data is merged into the normalized store. When dealing with paginated lists or filtered result sets, the default merge behavior may not align with specific UI requirements for data retention or deduplication.
The goal is to implement a custom merge function that prevents the overwriting of existing cached array elements while maintaining the correct order of entities across multiple fetch requests. There is uncertainty regarding how to efficiently handle pagination offsets within the merge function without causing redundant data entries or losing the referential integrity of the normalized cache.
typePolicy merge function to avoid duplication?33275 reputation · 16 Dec 2023, 09:53 UTC
For offset / cursor pagination you want one cache entry for the list field and an id-based merge that appends new items without duplication. Set the field policy so pagination arguments do not create separate cache keys, then merge by stable identifier.
Apollo does not automatically remove entities that disappear from a filtered query. The normalized objects remain in the store until garbage collection finds no references. The field value is updated by your merge policy, but stale items are not evicted by default.
If the list contains normalized objects, Apollo will reuse references when identifiers match. A custom merge is needed when you need order control, accumulation across pages, or when the server returns non-normalized arrays.
typePolicies: {
Query: {
fields: {
items: {
keyArgs: ['filter'], // omit offset/limit/after/first
merge(existing = [], incoming, { args }) {
// implementation below
}
}
}
}
}
merge(existing = [], incoming, { readField }) {
const merged = [...existing];
const seen = new Set(existing.map(item => readField('id', item)));
for (const item of incoming) {
const id = readField('id', item);
if (id == null || seen.has(id)) continue;
seen.add(id);
merged.push(item);
}
return merged;
}
For offset-based append, you can also use args.offset to decide prepend vs append, but deduplication by id remains required.
The cache updates the field value to the merged result, but it does not automatically evict entities that were previously cached and are now absent from the filtered response. Those entities remain in the normalized store until cache.gc() removes unreferenced objects. If you need strict removal, you must evict manually with cache.modify or cache.evict, or design the merge to replace rather than accumulate for filter changes.
Missing diagnostic that changes recommendation: Are list items normalized with __typename + id and which pagination arguments your query uses, e.g., offset/limit vs first/after? The merge and keyArgs choice depends on that.
Use comments to ask for clarification. Post a solution as an answer.
33,275 reputation · 16 Dec 2023, 04:30 UTC
When you want a single cache key for a paginated list, exclude offset/limit‑style arguments from keyArgs so every page targets the same field entry. In the merge function, use readField (or toReference) to build a map keyed by the entity’s stable ID, then return the map’s values as a new array. This prevents duplicates while preserving order. Apollo does not automatically drop entities that disappear from a filtered query; they stay in the normalized store until no references remain, so you must manually evict stale items if needed.