Apollo 10: Pagination
easy⏱ 5 mincourseapollo
The fetchMore pattern
The useQuery hook returns a fetchMore function that lets you load additional data into the same query. You pass new variables (like offset or cursor) and Apollo merges the results with existing data using your type policy's merge function. Without a merge function, new data overwrites existing data — so pagination always requires a type policy.
const { data, loading, fetchMore } = useQuery(GET_POSTS, {
variables: { offset: 0, limit: 10 },
});
const loadMore = () => {
fetchMore({
variables: { offset: data.posts.length },
});
};
Implement infinite scroll with fetchMore
Create a paginated PostFeed component. Use useQuery with offset: 0 and limit: 10. Add a "Load More" button that calls fetchMore({ variables: { offset: data.posts.length } }). Configure a merge function in type policies for Query.fields.posts that concatenates existing and incoming arrays. Track hasMore to hide the button when all items are loaded.
Offset vs cursor comparison
Offset pagination (offset: 20, limit: 10) is simple but breaks when items are inserted/deleted between pages — you might skip or duplicate items. Cursor pagination (after: 'abc123', first: 10) uses an opaque marker from the last item, making it resilient to changes. Apollo provides relayStylePagination() for Relay-compatible cursor pagination out of the box.
import { relayStylePagination } from '@apollo/client/utilities';
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
posts: relayStylePagination(),
},
},
},
});
Merge function in type policies
The merge function in type policies is the heart of Apollo pagination. It receives existing (current cached data, initially undefined) and incoming (new data from the server). For offset pagination: merge(existing = [], incoming) { return [...existing, ...incoming]; }. Use keyArgs to tell Apollo which variables create separate cache entries (e.g., keyArgs: ['category'] means each category has its own paginated list).