Lett's Go Build: Pagination with Relay and React. An Intermediary Tutorial
Tags: graphql, react, relay, pagination
Published: 2020-11-12 12:56:02


# TL;DR

This is a tutorial of how to create cursor-based pagination using Relay without a server and the final code can be seen [here](https://github.com/ttoss/relay-pagination/tree/509cd9e616eccc1befc62a54630b538e2f4c5ce4).

# Table of Contents

1. [Introduction](#introduction)
    1. [Objectives](#objectives)
    1. [Prerequisites](#prerequisites)
1. [Building the App](#building-the-app)
    1. [First Commit](#first-commit)
    1. [App First Run](#app-first-run)
    1. [Pagination](#pagination)
1. [Conclusion](#conclusion)
1. [Acknowledgments](#acknowledgments)

# Introduction

This tutorial is an extension of another tutorial I start to write about "Making Cursor-Based Pagination with AWS AppSync and DynamoDB." As I started to write the latter tutorial, I realized that it was getting too big so that I decided to split it into two tutorials. The first is focused only on the backend configuration of AWS AppSync and DynamoDB. The second one - this tutorial - only on the frontend.

Additionally, this tutorial was a way I found to document my learning process. Before starting writing this tutorial, I was studying pagination with Relay and, in order to organize my thoughts and processes, I wrote this article while learning Relay's pagination.

In this article, I'm going to create an app that has a user with his posts. The user may have many posts and, in a real app, it isn't good practice fetch all posts in a single request. When we have this case, pagination may be a good technique to be adopted to fetch a small amount of the posts each time.

## Objectives

- Show step by step how to create pagination with Relay without connecting with a previously existing backend.
- Relay pagination with hooks and React Concurrent Mode.
- Create an app in which is possible to provide a GraphQL server endpoint to test the server cursor-based pagination (in our case, the server is AWS AppSync and DynamoDB).

## Prerequisites

This is an intermediary tutorial because you should have a basic understanding of:

- React concurrent mode.
- Relay fragments.
- TypeScript.

# Building the App

## First Commit

For the first app setup, I created a project with [CRA](https://create-react-app.dev/), [activated React Concurrent Mode](https://reactjs.org/docs/concurrent-mode-adoption.html), and installed [Theme-UI](https://theme-ui.com/). This first implementation can be seen [here](https://github.com/ttoss/relay-pagination/tree/f6e0bec6d59c848ed0d198bdb431f44ee4d5ae5d).

## App First Run

### Installing Relay

You might want to check the step by step of the [official Relay's documentation](https://relay.dev/docs/en/experimental/a-guided-tour-of-relay#installation).

First, let's install React Relay experimental:

```
yarn add react-relay@experimental
```

Second, Relay compiler for TypeScript and GraphQL:

```
yarn add -D babel-plugin-relay graphql relay-compiler relay-compiler-language-typescript relay-config
```

Finally, the types:

```
yarn add -D @types/react-relay
```

### Configuring Relay

Create a `.babelrc` configuration to allow Relay to works.

```json
// .babelrc
{
  "plugins": ["relay"]
}
```

Also, we create a `relay.config.json`

```js
// relay.config.json
module.exports = {
  language: 'typescript',
  src: 'src/',
  schema: 'schema.graphql',
  exclude: ['**/node_modules/**', '**/__mocks__/**', '**/__generated__/**'],
};
```

### Creating the First Query

Before start creating the React components, let's define our first `graphql.schema`. In this stage, we're going to focus only on the User entity. Our User type has only `id` and `name` properties and implements the interface `Node`. Later in this text, I'll explain more about the `Node` and the role it takes to make pagination works.

```graphql
## schema.graphql
interface Node {
  id: ID!
}

type User implements Node {
  id: ID!
  name: String
}

type Query {
  user(id: ID!): User
}

schema {
  query: Query
}
```

`User.tsx`

Also, add `declare module 'babel-plugin-relay/macro';` to your `react-app-env.d.ts` file.

```tsx
// src/User.tsx
import * as React from 'react';

import { graphql } from 'babel-plugin-relay/macro';
import { useLazyLoadQuery } from 'react-relay/hooks';
import { Heading } from 'theme-ui';

const User = () => {
  const { user } = useLazyLoadQuery(
    graphql`
      query UserGetUserDataQuery($userId: ID!) {
        user(id: $userId) {
          id
          name
        }
      }
    `,
    { userId: 'user1' }
  );

  if (!user) {
    throw new Error('Cannot load user ;/');
  }

  return (
    <div>
      <Heading as="h3">{user.name}</Heading>
    </div>
  );
};

export default User;
```

Now, save the command `relay-compiler` as an NPM script, e.g, `"relay": "relay-compiler"` and execute the command `yarn run relay`. This command will generate files inside `src/__generated__/` folder with the query's types. To type our query, we need to import the type and set it in our `useLazyLoadQuery` method, as shown below:

```tsx
// src/User.tsx
...
import { Heading } from 'theme-ui';

import { UserGetUserDataQuery } from './__generated__/UserGetUserDataQuery.graphql';

const User = () => {
  const { user } = useLazyLoadQuery<UserGetUserDataQuery>(
...
```

### Faking Data

To finish the first query implementation, we need to add the Relay provider to our app and in our Relay environment. The code shown below will receive the request from Relay and return empty data. Also, to understand the Relay's request, we added a `console.log` to see what happens when we run the App.

```ts
// relay/fakeEnvironment.ts
import {
  Environment,
  Network,
  RecordSource,
  Store,
  FetchFunction,
} from 'relay-runtime';

const fetchQuery: FetchFunction = async (operation, variables) => {
  console.log({ operation, variables });
  return Promise.resolve({ data: {} });
};

const environment = new Environment({
  network: Network.create(fetchQuery),
  store: new Store(new RecordSource()),
});

export default environment;
```

Finally, the Relay provider with React Suspense.

```tsx
// src/App.tsx
import * as React from 'react';

import { RelayEnvironmentProvider } from 'react-relay/hooks';

import User from './User';

import RelayFakeEnvironment from './relay/fakeEnvironment';

const App = () => {
  return (
    <RelayEnvironmentProvider environment={RelayFakeEnvironment}>
      <React.Suspense fallback="loading...">
        <User />
      </React.Suspense>
    </RelayEnvironmentProvider>
  );
};

export default App;
```

Let's run your app with `yarn start` to see what will happen. If everything works as planned, we'll get the error `Cannot load user ;/` throw by `User` component. This is an expected error because the data that is being returned in your `fetchQuery` doesn't have the `user` property. Checking the browser console, we can see the data logged from our provider:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/8pf6u5c1b41uyjxn9p8x.png)

With this print in hands, we change our `fetchQuery` to return fake data to the query `UserGetUserDataQuery`:

```ts
// relay/fakeEnvironment.ts
...
const fetchQuery: FetchFunction = async (operation, variables) => {
  console.log({ operation, variables });

  let data = {};

  const { name } = operation;

  if (name === 'UserGetUserDataQuery') {
    data = { user: { id: variables.userId, name: 'Pedro' } };
  }

  return Promise.resolve({ data });
};
...
```

Now, if we reload the app, we'll see the page with the username chosen, in my case "Pedro".

The final code can be seen [here](https://github.com/ttoss/relay-pagination/tree/8d245bcc14310f281080296ae4396029afec4a46). We've also bootstrapped the interface with some styles that weren't covered in this section.

## Pagination

Now that we have our user data, we want to display some of their posts accordingly to a specific sorting rule, for instance, the newest, oldest, or the most relevant.

The point here is that we don't want (neither must do) fetch all posts of a user otherwise the database would receive a lot of requisitions, thus comprising our app's performance. To solve this problem, we use pagination to fetch some posts and if the user wants more posts, they request more data to our backend.

### Understanding Connections, Edges, and Nodes

I've read these astonishing articles to understand the concepts behind Relay's cursor-based pagination better and I do recommend you read them too:

- [Gabriel Nordeborn. Pagination with minimal effort in Relay.](https://dev.to/zth/pagination-with-minimal-effort-in-relay-gl4)
- [Caleb Meredith. Explaining GraphQL Connections](https://www.apollographql.com/blog/explaining-graphql-connections-c48b7c3d6976/)
- [Michael Hahn. Evolving API Pagination at Slack](https://slack.engineering/evolving-api-pagination-at-slack/)
- [GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm)

Now I'm going to explain these concepts with my words :smile:

#### Nodes

An item, type, entity of our graph model.

#### Connections

Thinking in graphs, our nodes may have some relations with other nodes. These relations could be: a node `User` has the association with another `User` by a `Friendship` property; a node `Author`, with some nodes `Articles`, by a `WrittenBy`. The set of relations with the same property (`Friendship`, `WrittenBy`) of a node is called **connections**.

Connections may have metadata associated with the set of the elements returned. For instance, a connection returns some elements and the metadata about these elements could be: id of the first and last element. 

#### Edges

Edges are the elements returned by a connection. Edges are the joint of a node and some metadata explaining better the connection between both nodes ("both nodes" means the returned with the edge and the source, the node from which we request the connection).

### Bringing Connections, Edges, and Nodes to our Schema

The question here is: how do we create the connection between User and Post? When I started studying pagination, this was the first question I've asked myself. Pretending to answer it, I followed some steps to create the connections that I'm going to show you. These steps were created based on the [Relay spec](https://relay.dev/graphql/connections.htm).

#### 1. Create a property in our source node that will represent the connection that must return the edges and some metadata.

In our case, we'll add the property `posts: PostsConnection` in our `User` type and define the type `PostsConnection`. As we discussed [here](#connections), the connection type must return `edges` and some metadata. Specifically for cursor-based pagination, we need to provide metadata related to the requested pagination, some page info, whose type we'll call `PageInfo`. This type must have these properties:

- `hasNextPage: Boolean!`
- `hasPreviousPage: Boolean!`
- `startCursor: String`
- `endCursor: String`

`hasNextPage` and `hasPreviousPage` are self-explanatory and it'll be clearer when we implement the example. `startCursor` and `endCursor` will be covered when we defined the edge type because the cursor is a metadata of the edge type.

```graphql
## schema.graphql
...
type User implements Node {
  id: ID!
  name: String
  posts(
    first: Int,
    after: String,
    last: Int,
    before: String
  ): PostsConnection
}

type PostsConnection {
  edges: [PostEdge]
  pageInfo: PageInfo!
}

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

The arguments `first` and `after` are used to perform forward pagination and are described [here](https://relay.dev/graphql/connections.htm#sec-Forward-pagination-arguments). `last` and `before`, to perform backward pagination, and are defined [here](https://relay.dev/graphql/connections.htm#sec-Backward-pagination-arguments).

Another example of metadata that might be added along with `pageInfo` is `systemCost`, whose properties might be `queryTime` and `memoryUsed`. It'd represent the cost of the query to our system. I used this example to help we understand better what metadata is in a connection.

#### 2. The edge must return the node and some metadata.

Here is our schema:

```graphql
## schema.graphql
...
type PostEdge {
  cursor: String!
  node: Post!
}

type Post implements Node {
  id: ID!
  title: String!
  description: String
}
...
```

The `cursor` property is like an id for the edge. With the `cursor` property we must be able to retrieve and locate that edge on your backend.

Besides `cursor`, other metadata might be `relationship`. If the `Post` has an author and/or co-author, this metadata would be responsible to tell the relationship between the `User` and the `Post`. A rule of thumb for me is: if we need some data to complement the relation between two nodes that doesn't make sense be added to any node, probably it'll fit in the edge metadata.

```graphql
## schema.graphql
...
enum AuthorPostRelationship {
  Author
  CoAuthor
}

type PostEdge {
  cursor: String!
  relationship: AuthorPostRelationship!
  node: Post!
}
...
```

### Creating `UserPosts` Component

Let's create a user posts components to perform pagination using fake data. The first scratch is shown below. Note that this code doesn't work yet because we don't have yet:

1. The `@refetchable` directive.
1. The `@argumentDefinitions` directive.
1. The `@connection` directive.
1. The `node` query.
1. A fake resolver.

```tsx
// src/User.tsx
import * as React from 'react';

import { graphql } from 'babel-plugin-relay/macro';
import { useLazyLoadQuery, usePaginationFragment } from 'react-relay/hooks';
import { Box, Button, Flex, Heading, Styled } from 'theme-ui';

import { UserGetUserDataQuery } from './__generated__/UserGetUserDataQuery.graphql';

const UserPosts = ({ user }: any) => {
  const {
    data,
    hasNext,
    loadNext,
    isLoadingNext,
    hasPrevious,
    loadPrevious,
    isLoadingPrevious,
  } = usePaginationFragment(
    graphql`
      fragment User_posts on User {
        posts(first: $first, after: $after, last: $last, before: $before) {
          edges {
            node {
              id
              title
              description
            }
          }
        }
      }
    `,
    user
  );

 ...
};

const User = () => {
  const { user } = useLazyLoadQuery<UserGetUserDataQuery>(
    graphql`
      query UserGetUserDataQuery($userId: ID!) {
        user(id: $userId) {
          id
          name
          ...User_posts
        }
      }
    `,
    { userId: 'user1' }
  );

  if (!user) {
    throw new Error('Cannot load user ;/');
  }

  return (
    <div>
      <Heading as="h3" sx={{ fontSize: 5 }}>
        User Name: {user.name}
      </Heading>
      <UserPosts user={user} />
    </div>
  );
};

export default User;
```

#### The `@refetchable` Directive

The first directive to be added is the `@refetchable`. Fragments can't be queried by themselves, we need a parent query in which the fragment will be added. When we add this directive, Relay's engine automatically generates a new query for us when we require new pagination data. [You might want to read more about this on Relay docs.](https://relay.dev/docs/en/experimental/a-guided-tour-of-relay#refreshing-fragments)

```tsx
// src/User.tsx
...
      fragment User_posts on User
      @refetchable(queryName: "UserPostsPaginationQuery") {
        posts(first: $first, after: $after, last: $last, before: $before)
...
```

The parameter `queryName` defines the name of the query that will be created.

#### The `@argumentDefinitions` Directive.

This directive provides a way to add variables to our fragment. If we weren't able to this, we would have to provide them in our parent component where the parent query is located. For instance, as we want to provide `first`, `after`, `last`, `before`, if we didn't have the directive, we would have to provide them to our `UserGetUserDataQuery` query inside our `User` component. The `User` component doesn't perform the pagination, it even doesn't know what the `UserPosts` component is doing. 

```tsx
// src/User.tsx
...
      fragment User_posts on User
      @argumentDefinitions(
        first: { type: "Int" }
        after: { type: "String" }
        last: { type: "Int" }
        before: { type: "String" }
      )
      @refetchable(queryName: "UserPostsPaginationQuery") {
...
```

#### The `@connection` Directive.

The `@connection` directive indicates to Relay that a pagination operation will be performed over a specific connection, in our case, `posts`.

```tsx
// src/User.tsx
...
        @refetchable(queryName: "UserPostsPaginationQuery") {
        posts(first: $first, after: $after, last: $last, before: $before)
          @connection(key: "User_posts_postsConnection") {
          edges {
            node {
              id
              title
              description
            }
          }
        }
      }
...
```

`key` is an identifier of this connection. It's used to help cache updates (not our case). You may want to read more about [connections from official docs](https://relay.dev/docs/en/experimental/a-guided-tour-of-relay#connections).

##### The `node` Query

At this point, if we execute `yarn run relay`, we'll get this error: `Internal Error: Unknown field 'node' on type 'Query'.` Let's talk a little about it.

You may want to read [Global Object Identification](https://graphql.org/learn/global-object-identification/) and [The magic of the Node interface](https://dev.to/zth/the-magic-of-the-node-interface-4le1) to understand more about `node` query. In our case, it'll be used to create new queries when the user requests new pagination data. This concept will be clearer in the next topic because we'll see an example of the created query and fetching new data.

We need to add the `node` query in your `schema.graphql`:

```graphql
## schema.graphql
type Query {
  user(id: ID!): User
  node(id: ID!): Node
}
```

#### A fake resolver

The final Relay's environment code can be seen [here](https://github.com/ttoss/relay-pagination/blob/0029c748b2e0ecb83886d6d21f174a2e63e5ffdc/src/relay/fakeEnvironment.ts).

The first point is that we created a method called `getPostsConnection`. This method receives cursor variables (`after`, `before`, `first`, and `last`) and returns posts connection to our user. We also defined a limit of posts creation to be able to notice when the properties `hasNextPage` and `hasPreviousPage` become falsy.

The second point is that we can inspect the query we receive when we trigger a pagination action. The example below is the query sent by Relay when we request more posts:

```graphql
query UserPostsPaginationQuery(
  $after: String
  $before: String
  $first: Int
  $last: Int
  $id: ID!
) {
  node(id: $id) {
    __typename
    ...User_posts_pbnwq
    id
  }
}

fragment User_posts_pbnwq on User {
  posts(first: $first, after: $after, last: $last, before: $before) {
    edges {
      node {
        id
        title
        description
        __typename
      }
      cursor
    }
    pageInfo {
      endCursor
      hasNextPage
      hasPreviousPage
      startCursor
    }
  }
  id
}
```

and the variables:

```json
{
  "after": "3",
  "before": null,
  "first": 4,
  "id": "user1",
  "last": null
}
```

We may notice the name of the created query - `UserPostsPaginationQuery` - it is the name we've defined in our `@refetchable` directive. 

Also, there is the `node` query inside `UserPostsPaginationQuery`. This is how Relay's works: it retrieves the `id` of the parent node of the fragment, in our case, `user1`, whose type is `User` and pass it to `node`. `node` can assume any type of our schema that implements `Node` as a result of [Global Object Identification](https://graphql.org/learn/global-object-identification/).

Finally, we create a response to the query above:

```ts
// src/relay/fakeEnvironment.ts
...
  if (name === 'UserPostsPaginationQuery') {
    data = {
      node: {
        id: variables.id,
        name: 'Pedro',
        posts: getPostsConnection(variables as any),
        __typename: 'User',
      },
    };
  }
...
```

At this point, loading next and previous posts should work and the pagination is disabled when posts ids reach about `-15` or `15.

The final code can be seen [here](https://github.com/ttoss/relay-pagination/tree/0029c748b2e0ecb83886d6d21f174a2e63e5ffdc).

# Conclusion

For me, writing this article helped me understand better how cursor-based pagination works because it is a concept that I studied while written this post. Also, it'll be a guide to use when I need to implement Relay pagination using hooks.

For you, I hope this article improved your acknowledgment of cursor-based pagination, Relay with hooks, and the concepts behind nodes, edges, and connections.

Finally, this project will be used as a client for the one I'm writing about cursor-based pagination with AWS AppSync and DynamoBD.

# Acknowledgments

Thanks to [@sseraphini](https://twitter.com/sseraphini) for encouraging me to write more and review this tutorial. I do recommend you to send a DM to him, you'll be amazed with the conversation you'll have. 

<span>Cover photo by <a href="https://unsplash.com/@romcosmic?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Roman Trifonov</a> on <a href="https://unsplash.com/s/photos/pages?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></span>

---

Please, feel free to give me any feedback. This was my first tutorial and I'll appreciate any feedback to help me improve or just to know how you feel reading this tutorial :) You can also contact me on Twitter [@arantespp](https://twitter.com/arantespp)