Pagination
Recommended background reading
- Queries and mutations in GraphQL
- A Guided Tour of Relay: Rendering List Data and Pagination
- The Relay server specification: Connections
- React documentation: Suspense for Data Fetching
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:
- 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 theticketsConnection
field happen to be onQuery
, pagination can be done on fields on any GraphQL type. - 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. - We add another directive,
@argumentDefinitions
, where we define two arguments that we need for pagination,count
andcursor
. 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. - We select the
ticketsConnection
field onQuery
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 akeyName
, 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. - Finally, we spread another component's fragment
SingleTicket_ticket
on the connection'snode
, 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:
- Just like with anything using fragments, we'll need a fragment reference to pass to our fragment.
- 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. - 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.
- We render each node using the
<SingleTicket />
component, who's data demands we spread on thenode
of our refetchable fragment. We get the object withSingleTicket
's fragment reference, whichSingleTicket
will need to get its fragment data, by passingticket.fragmentRefs
to<SingleTicket />
. Feeling confused byfragmentRefs
? Go back and freshen up on using fragments here. - 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'susePaginationFragment
under the hood, which you can read more about here.
Parameters
usePagination
returns a record with the following properties.
Name | Type | Note |
---|---|---|
data | 'fragmentData | The 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. |
hasNext | bool | Are there more nodes forward in the connection to fetch? |
hasPrevious | bool | Are there more nodes backwards in the connection to fetch? |
isLoadingNext | bool | |
isLoadingPrevious | bool | |
refetch | (~variables: 'variables, ~fetchPolicy: fetchPolicy=?, ~onComplete: option(Js.Exn.t) => unit=?) => | Refetch the entire connection with potentially new variables. |