Labs ICT
๐ŸŽ“ SIWES/IT Login

Pagination in GraphQL

3 min read | GraphQL Tutorial
๐ŸŽ“

Doing SIWES / IT attachment?

Get a structured training programme, a supervised digital logbook, and a completion certificate with LabsICT SIWES / IT.

Explore SIWES / IT Training

Pagination in GraphQL

Pagination is essential for handling large datasets. Instead of loading all records at once, you load them in chunks. GraphQL offers several pagination patterns.

Offset-Based Pagination

The simplest approach uses offset and limit parameters to skip and limit results:

type Query {
  posts(offset: Int, limit: Int): [Post!]!
}

# Schema
type Post {
  id: ID!
  title: String!
  content: String!
}

# Query
query {
  posts(offset: 0, limit: 10) {
    id
    title
  }
}

Offset pagination is simple but has a problem: if data changes between requests, you might miss or duplicate items.

Cursor-Based Pagination

Cursor-based pagination uses a pointer to the last item you fetched. It's more reliable than offset-based pagination.

type PostConnection {
  edges: [PostEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type PostEdge {
  node: Post!
  cursor: String!
}

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

type Query {
  posts(first: Int, after: String, last: Int, before: String): PostConnection!
}

# Query first 10 posts
query {
  posts(first: 10) {
    edges {
      node {
        id
        title
      }
      cursor
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

# Query next page
query {
  posts(first: 10, after: "cursor-from-previous-query") {
    edges {
      node {
        id
        title
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

This is the Relay specification for pagination. It's more complex but handles edge cases better.

Implementing Cursor Pagination

Here's how to implement cursor-based pagination in your resolvers:

const resolvers = {
  Query: {
    posts: async (parent, args, context) => {
      const { first, after, last, before } = args;
      
      // Build the query
      let query = {};
      if (after) {
        query.createdAt = { $gt: decodeCursor(after) };
      }
      if (before) {
        query.createdAt = { $lt: decodeCursor(before) };
      }
      
      // Fetch one extra item to check if there's a next page
      const limit = first || last;
      const items = await context.db.posts
        .find(query)
        .sort({ createdAt: 1 })
        .limit(limit + 1);
      
      const hasNextPage = items.length > limit;
      const edges = items.slice(0, limit).map(item => ({
        node: item,
        cursor: encodeCursor(item.createdAt),
      }));
      
      return {
        edges,
        pageInfo: {
          hasNextPage,
          hasPreviousPage: !!before,
          startCursor: edges[0]?.cursor,
          endCursor: edges[edges.length - 1]?.cursor,
        },
        totalCount: await context.db.posts.countDocuments(),
      };
    },
  },
};

The key is to fetch one extra item to determine if there's a next page.

Client-Side Pagination

On the client, use Apollo Client's fetchMore to load additional pages:

import { useQuery, gql } from '@apollo/client';

const GET_POSTS = gql`
  query GetPosts($first: Int, $after: String) {
    posts(first: $first, after: $after) {
      edges {
        node {
          id
          title
        }
        cursor
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
`;

function PostList() {
  const { data, fetchMore } = useQuery(GET_POSTS, {
    variables: { first: 10 },
  });

  const loadMore = () => {
    fetchMore({
      variables: {
        after: data.posts.pageInfo.endCursor,
      },
    });
  };

  return (
    <div>
      {data?.posts.edges.map(({ node }) => (
        <div key={node.id}>{node.title}</div>
      ))}
      {data?.posts.pageInfo.hasNextPage && (
        <button onClick={loadMore}>Load More</button>
      )}
    </div>
  );
}

Apollo Client automatically merges the new results with the existing cache.

๐Ÿงช Quick Quiz

What is pagination in GraphQL?