Pagination with Relay Connections
The Relay Cursor Connection Specification is the GraphQL community standard for pagination. It uses edges, nodes, and cursors to enable efficient, consistent pagination that works for both infinite scroll and page-based UIs.
1 Connection Schema Pattern
GraphQL SDL — Relay Connection Types
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type PostEdge {
cursor: String! # Opaque position marker
node: Post! # The actual data
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type Query {
# Forward pagination: first + after
# Backward pagination: last + before
posts(
first: Int
after: String
last: Int
before: String
filter: PostFilter
): PostConnection!
}
2 Connection Resolver Implementation
JavaScript — Cursor Pagination Resolver
function encodeCursor(id) { return Buffer.from("cursor:" + id).toString("base64"); }
function decodeCursor(cursor) { return Buffer.from(cursor, "base64").toString().split("cursor:")[1]; }
const resolvers = {
Query: {
posts: async (_, { first = 10, after, filter = {} }) => {
const query = { ...filter };
if (after) {
const lastId = decodeCursor(after);
query._id = { $gt: lastId }; // fetch records AFTER cursor
}
// Fetch one extra to determine hasNextPage
const items = await Post.find(query).sort({ _id: 1 }).limit(first + 1);
const hasNextPage = items.length > first;
const edges = items.slice(0, first).map(post => ({
cursor: encodeCursor(post.id),
node: post
}));
return {
edges,
totalCount: await Post.countDocuments(filter),
pageInfo: {
hasNextPage,
hasPreviousPage: !!after,
startCursor: edges[0]?.cursor || null,
endCursor: edges[edges.length - 1]?.cursor || null
}
};
}
}
};
3 Code Challenge
Challenge: Build an infinite-scroll UI component in React using
useQuery with fetchMore. When the user scrolls to the bottom, call fetchMore with the endCursor as the after variable, and merge the new results into the existing Apollo cache.