Implementing pagination with AWS AppSync
Tags: aws, appsync, amplify
Published: 2020-05-18 20:45:04


## Why Pagination

In this post, I'm going to show how you can get started with pagination in GraphQL using an AWS AppSync API and the AWS Amplify framework. The primary reason to use pagination is to control and limit the amount of data that is fetched from your backend and returned to your client at once. Pagination can help build efficient and cost-effective solutions by controlling the amount of work done to retrieve data in the backend. It can also improve overall responsiveness by returning smaller sets of data faster to the application.

## Types of pagination

2 common forms of pagination are offset-based and token-based pagination. With offset-based pagination, you specify the page size and a starting point: the row after which, or the page at which you want to start fetching data. When using a page, the page along with the page size identifies the row after which to start fetching data (e.g.: `offset = (page - 1) * page_size - 1` in a zero-based index). You can find offset-based pagination when dealing with relational databases. For example, in mysql you can fetch data from an offset using the `LIMIT` [clause](https://dev.mysql.com/doc/refman/8.0/en/select.html). In this example, 5 is the offset (fetch after that row) and 10 is the page size (return 10 items).

```sql
SELECT * FROM tbl LIMIT 5,10;  # Retrieve rows 6-15
```

With token-based pagination, a token is used to specify the record after which additional items should be fetched, along with the page size. The implementation of the token is system-specific.  DynamoDB is an example of at system that uses token-pagination to [paginate the results from Query operations](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.Pagination.html). With DynamoDB, the result of a query may return a `LastEvaluatedKey` element. This is a token indicating that additional items can be fetched for this specific query. You can then continue the query and get the rest of the items by repeating the query and setting `ExclusiveStartKey` to the last value of `LastEvaluatedKey`.

## How pagination works with AWS AppSync
AWS AppSync is a fully managed GraphQl service that makes it easy to build data-driven solutions in the cloud. Using the AWS Amplify GraphQL transform, you can quickly build AppSync APIs with types backed by data sources in your accounts. For example, you can use the `@model` directive in your schema to generate an API with types backed by DynamoDB tables.

Let’s take a look a how to work with pagination using Amplify and AppSync. I built a simple React app to showcase pagination with AppSync: [Pagination with AWS AppSync](https://pagination.dced4ubmfmz07.amplifyapp.com/). You can find the entire code here: https://github.com/onlybakam/todo-app-pagination. I am using the [Amplify  API library](https://docs.amplify.aws/lib/graphqlapi/getting-started/q/platform/js#summary) to easily interact with the AppSync API.

![preview](https://dev-to-uploads.s3.amazonaws.com/i/g12p61m573zy93mb9rkh.png)

I created a new amplify project and created an AppSync API using the CLI. To find out how to get started with this, check out the [Getting Started guide](https://docs.amplify.aws/lib/graphqlapi/getting-started/q/platform/js). I then created the following schema:

```graphql
type Todo
  @model
  @key(
    fields: ["owner", "dueOn"]
    name: "ByDate"
    queryField: "listTodosByDate"
  ) {
  id: ID!
  name: String!
  description: String
  owner: String!
  dueOn: AWSDateTime!
}
```

The `@key` directive allows you to create a query to fetch todos per owner sorted by their due date.  Check out [Amplify Framework Docs - Data access patterns](https://docs.amplify.aws/cli/graphql-transformer/dataaccess) to find out more about how the `@key` can enable various data access patterns.

To fetch a list of todos for an owner, you execute the `ListTodosByDate` query. You can specify the amount of items you want returned using the `limit` argument. By default, the limit is set to 100. You can also specify the order the items are sorted by using  `sortDirection` (set to `ASC` or `DESC`).

```graphql
  query ListTodosByDate(
    $owner: String
    $dueOn: ModelStringKeyConditionInput
    $sortDirection: ModelSortDirection
    $filter: ModelTodoFilterInput
    $limit: Int
    $nextToken: String
  ) {
    listTodosByDate(
      owner: $owner
      dueOn: $dueOn
      sortDirection: $sortDirection
      filter: $filter
      limit: $limit
      nextToken: $nextToken
    ) {
      items {
        id
        name
        description
        owner
        dueOn
      }
      nextToken
    }
  }
```

The query returns a list of items and a `nextToken`  field. If `nextToken` is set, this indicates there are more items to fetch. In a subsequent query, you can pass this value in the query arguments to continue fetching items starting after the final item that was last returned.

In the application, we want to be able to paginate forward and backwards through todos. To do this, we maintain 3 state variables

```javascript
  const [nextToken, setNextToken] = useState(undefined)
  const [nextNextToken, setNextNextToken] = useState()
  const [previousTokens, setPreviousTokens] = useState([])
```

* `nextToken` is the the token used to fetch the current items
* `nextNextToken` is the token returned by the last fetch. If this token is set, you can paginate forward.
* `previousTokens` is an array of previous tokens. These tokens allow us to paginate the todo list backwards. If there is a token in the array, you can paginate backwards.

A new set of todos is fetched whenever the `owner`, `nextToken` or `sortDirection` changes.

```javascript
import { listTodosByDate } from './graphql/queries'
import { API, graphqlOperation } from '@aws-amplify/api'

useEffect(() => {
  const fetch = async () => {
    const variables = {
      nextToken,
      owner,
      limit,
      sortDirection,
    }
    const result = await API.graphql(graphqlOperation(listTodosByDate, variables))
    setNextNextToken(result.data.listTodosByDate.nextToken)
    setTodos(result.data.listTodosByDate.items)
  }

  fetch()
}, [nextToken, owner, sortDirection])
```

**Loading the initial list of items**

![loading initial list](https://dev-to-uploads.s3.amazonaws.com/i/9cplo7n65oabx604kgi3.png)

When the owner changes, all the fields are reset. `nextToken` is set to _undefined_ which makes the query fetch items from the beginning. When the query returns, the value of `nextToken` in the result is assigned to `nextNextToken`.  It’s important here to not immediately assign the value to the `nextToken` state as this would trigger another fetch right away.

**Pagination forward**

![pagination forward](https://dev-to-uploads.s3.amazonaws.com/i/oonpbyn8hsovrzyygd2c.png)

If `nextNextToken` is set, you can paginate forward. When the user presses the “Next” button, the current value of `nextToken` is pushed on the `previousTokens` array. Next,  `nextToken` is set to the current value of `nextNextToken`. Finally `nextNextToken` is then set to _undefined_. When the query returns, again  the value of `nextToken` in the result is assigned to `nextNextToken`. This process can be repeated as long as the query indicates that there are more items to paginate.

**Pagination backwards**

![pagination backwards](https://dev-to-uploads.s3.amazonaws.com/i/p9ge5zmr0r57glnl2as3.png)

The `previousTokens` array stores the previously used tokens in order (think of is as a history stack). To paginate backwards, the last value is popped off the array and assigned to `nextToken` which triggers a new query. This allows you to repeat the query from a known "starting point". The query results may return a different `nextToken`. This is because items may have been inserted or deleted since the nextToken `A` was returned. By assigning the value of `nextToken` in the result is to `nextNextToken`, you keep paginating forward from the right position.

![list of tokens](https://dev-to-uploads.s3.amazonaws.com/i/un1r9664j6b8r7nbt6rm.png)

## Conclusion
This post provided an overview of pagination and a simple solution for handling pagination in a React app with an AppSync API. Getting started with AWS AppSync and AWS Amplify is really easy. Check out the docs [here](https://docs.amplify.aws/). 

You can find the code for this application here: https://github.com/onlybakam/todo-app-pagination. You can check out an implementation of it here: [Pagination with AWS AppSync](https://pagination.dced4ubmfmz07.amplifyapp.com/).