Solving the 'Vanishing List' Problem with Apollo Client Type Policies
Stop your paginated lists from disappearing. Learn how to use Apollo Client's typePolicies and merge functions to handle infinite scroll and custom cache keys.
14 Apr 2026, 13:11 UTC

The Problem: Why Your Paginated Lists Disappear
You implement a "Load More" button for a list of products. The first request returns ten items, and they render perfectly. You trigger the second request for the next ten items, but instead of seeing twenty products, the first ten vanish, replaced entirely by the new set.
This happens because Apollo Client's default cache behavior for arrays is to replace the existing data with the new response. To the cache, a field returning a list is just a value; when a new value arrives for that field, the old one is overwritten. To fix this, you need to move beyond default normalization and implement typePolicies.
Understanding Cache Normalization
By default, Apollo Client flattens your data. If a query returns a User with an id of "123", Apollo doesn't store that user inside the query result. Instead, it creates a lookup table entry: User:123. This ensures that if the user's name is updated in one query, every component observing that user updates instantly.
However, normalization works on objects, not arrays. An array of products is treated as a single value associated with a specific query. When you fetch page two, Apollo sees a new array for that query and swaps the old list for the new one.
Customizing Behavior with typePolicies
The typePolicies configuration in the InMemoryCache allows you to tell Apollo exactly how to handle specific types or fields. This is the primary tool for managing complex data relationships and pagination.
Custom Cache Keys
Not every server uses id as the primary key. If your API uses _id or slug, the cache cannot normalize the data automatically, leading to duplicate entries. You can define a keyFields array to specify the unique identifier for a type.
The Merge Function
To solve the vanishing list problem, you use a merge function. This function intercepts the incoming data and the existing cache state, allowing you to combine them (e.g., concatenating two arrays) rather than replacing them.
Worked Example: Implementing Infinite Scroll
Assume we have a Query type with a field allProducts that takes an offset argument. We want to append new products to the existing list in the cache.
import { InMemoryCache } from '@apollo/client';
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
allProducts: {
// The merge function defines how to combine old and new data
merge(existing = [], incoming, { args }) {
// If the incoming data is an array, concatenate it to the existing list
return [...existing, ...incoming];
},
},
},
},
Product: {
// Ensure products are normalized by 'slug' instead of 'id'
keyFields: ['slug'],
},
},
});
Execution Details:
- Where to run: This configuration is passed to the
ApolloClientconstructor during app initialization. - Permissions: Client-side JavaScript execution.
- Expected Check: When calling
allProducts(offset: 10), the cache should now contain 20 items instead of 10. - Risk: If the server returns duplicate items across pages, the
mergefunction above will create duplicate entries in the array. You may need to filter by ID during the merge.
Trade-offs and Limitations
While typePolicies provide immense control, they introduce client-side complexity. Over-reliance on complex read or merge functions can create performance bottlenecks, as these functions run every time the cache is accessed or updated.
Additionally, typePolicies cannot fix a broken API. If your server does not provide consistent unique identifiers, normalization is impossible, and you will be forced to use fetchPolicy: 'no-cache' or manage state manually, losing the benefits of Apollo's reactive UI.
Verifying the Result
To confirm your policies are working, use the Apollo Client DevTools browser extension:
- Open the Cache tab.
- Observe the structure: You should see a flat list of
Product:slug-nameentries rather than a deeply nested tree. - Trigger a "Load More" action and verify that the
allProductsarray in theROOT_QUERYgrows in length rather than being replaced.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.