Skip to main content

Pagination

Videos from the Relay video series covering pagination:

Pagination in Relay

The features outlined on this page requires that your schema follow the Relay specification. Read more about using RescriptRelay with schemas that don't conform to the Relay specification here.

Relay has some great built-in tools to make pagination very simple if you use connection-based pagination and your schema conforms to the the Relay server specification. Let's look at some examples.

Setting up for pagination

Pagination is always done using a fragment. Here's a definition of a Relay fragment that'll allow you to paginate over the connection ticketsConnection:

module Fragment = %relay(
`
fragment RecentTickets_query on Query
@refetchable(queryName: "RecentTicketsRefetchQuery")
@argumentDefinitions(
count: {type: "Int!", defaultValue: 10},
cursor: {type: "String!", defaultValue: ""}
) {
ticketsConnection(first: $count, after: $cursor)
@connection(key: "RecentTickets_ticketsConnection")
{
edges {
node {
id
...SingleTicket_ticket
}
}
}
}
`
)

Quite a few directives and annotations used here. Let's break down what's going on:

  1. First off, this particular fragment is defined on the Query root type (the root query type is really just like any other GraphQL type). This is just because the ticketsConnection field happen to be on Query, pagination can be done on fields on any GraphQL type.
  2. We make our fragment refetchable by adding the @refetchable directive to it. You're encouraged to read refetching and loading more data for more information on making fragments refetchable.
  3. We add another directive, @argumentDefinitions, where we define two arguments that we need for pagination, count and cursor. This component is responsible for paginating itself, so we want anyone to be able to use this fragment without providing those arguments for the initial render. To solve that we add default values to our arguments. You're encouraged to read more about @argumentDefinitions here.
  4. We select the ticketsConnection field on Query and pass it our pagination arguments. We also add a @connection directive to the field. This is important, because it tells Relay that we want it to help us paginate this particular field. By annotating with @connection and passing a keyName, Relay will understand how to find and use the field for pagination. This in turn means we'll get access to a bunch of hooks and functions for paginating and dealing with the pagination in the store. You can read more about @connection here.
  5. Finally, we spread another component's fragment SingleTicket_ticket on the connection's node, since that's the component we'll use to display each ticket.

We've now added everything we need to enable pagination for this fragment.

Pagination in a component

Let's look at a component that uses the fragment above for pagination:

@react.component
let make = (~query) => {
let {data, hasNext, isLoadingNext, loadNext} = Fragment.usePagination(query)

/**
* Any time you use the @connection directive in a fragment, a function will be autogenerated
* that help you turn that connection into an array of non-nullable nodes. That function will
* be exposed right on the fragment module, and called `getConnectionNodes`.
*/
let tickets = data.ticketsConnection->Fragment.getConnectionNodes

<div className="card">
<div className="card-body">
<h4 className="card-title"> {React.string("Recent Tickets")} </h4>
<div>
{tickets
->Array.map(ticket => <SingleTicket key=ticket.id ticket=ticket.fragmentRefs />)
->React.array}
{hasNext
? <button onClick={_ => loadNext(~count=2)->RescriptRelay.Disposable.ignore} disabled=isLoadingNext>
{React.string(isLoadingNext ? "Loading..." : "More")}
</button>
: React.null}
</div>
</div>
</div>
}

Whew, plenty more to break down:

  1. Just like with anything using fragments, we'll need a fragment reference to pass to our fragment.
  2. We pass our fragment reference into Fragment.usePagination. This gives us a record back containing functions and props that'll help us with our pagination. Check out the full API reference here.
  3. RescriptRelay automatically generate a function you can use to turn your connection into an array of nodes. We use that autogenerated function here to collect all our nodes in order to be able to map over and render them.
  4. We render each node using the <SingleTicket /> component, who's data demands we spread on the node of our refetchable fragment. We get the object with SingleTicket's fragment reference, which SingleTicket will need to get its fragment data, by passing ticket.fragmentRefs to <SingleTicket />. Feeling confused by fragmentRefs? Go back and freshen up on using fragments here.
  5. Finally, we use the helpers provided by usePagination to render a Load more-button if there's more data to load, and disable it if a request is already in flight.

There, basic pagination! Relay really does all the heavy lifting for us here, which is great. Continue reading for some advanced pagination concepts and a full API reference, or move on to subscriptions.

API Reference

A %relay() which is annotated with a @refetchable directive, and which contains a @connection directive somewhere, has the following functions added to it's module, in addition to everything mentioned in using fragments:

usePagination

As shown above, usePagination provides helpers for paginating your fragment/connection.

usePagination uses Relay's usePaginationFragment under the hood, which you can read more about here.

Parameters

usePagination returns a record with the following properties.

NameTypeNote
data'fragmentDataThe data as defined by the fragment.
loadNext(~count: int, ~onComplete: option(Js.Exn.t) => unit=?) => Disposable.t;A function for loading the next count nodes of the connection.
loadPrevious(~count: int, ~onComplete: option(Js.Exn.t) => unit=?) => Disposable.t;A function for loading the previous count nodes of the connection.
hasNextboolAre there more nodes forward in the connection to fetch?
hasPreviousboolAre there more nodes backwards in the connection to fetch?
isLoadingNextbool
isLoadingPreviousbool
refetch(~variables: 'variables, ~fetchPolicy: fetchPolicy=?, ~onComplete: option(Js.Exn.t) => unit=?) =>Refetch the entire connection with potentially new variables.