Skip to content

Rendering Long Lists

You’ll often run into the situation where you want to render a long list of data. It’s also common for that list to be much too long to query or even render on the screen. In order to address this, most APIs will accept a set of arguments on the field designed to window the list and leave it up to the client to keep a running total if the situation calls for it.

As GraphQL has matured, these arguments have somewhat standardized and fall roughly into two categories: cursor-based pagination and offset-based pagination. Houdini supports both but since our API relies on cursor-based pagination that’s what we’re going to show here. If you want to read more about pagination, head over to the pagination guide.

GraphQL: Cursor-Based Pagination

If you are familiar with cursor-based pagination you can safely skip this section.

Cursor-based pagination is an approach popularized in GraphQL by Facebook’s client, Relay. Their flavor of cursor-based pagination is known as “Connections” and relies on some additional types to help us traverse the list. For a practical example, take a look at the API for our Pokédex, specifically the moves field on the Species type which lets us look up the list of moves that a species can learn:

type Species {
moves(first: Int, after: String): SpeciesMoveConnection!
}
type SpeciesMove {
learned_at: Int!
method: String!
move: Move!
}
type SpeciesMoveConnection {
edges: [SpeciesMoveEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type SpeciesMoveEdge {
cursor: String
node: SpeciesMove
}
type PageInfo {
endCursor: String
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
}

Any time we have a field that would return a list we want to paginate, we instead return a Connection type that provides some meta data about the list like our current position (defined by the cursor) and whether or not there is more data to query. To see how this works in practice, let’s look at a query that gets the first 10 items in this list:

query {
species(id: 1) {
moves(first: 10) {
pageInfo {
hasNextPage
endCursor
}
edges {
cursor
# this is the move object
node {
learned_at
move {
name
}
}
}
}
}
}

The result of this query looks something like:

{
"species": {
"moves": {
"pageInfo": {
"hasNextPage": true,
"endCursor": "k"
},
"edges": [
{
"cursor": "a",
"node": {
"learned_at": 1,
"move": {
"name": "Growl"
}
}
},
...,
{
"cursor": "k",
"node": {
"learned_at": 0,
"move": {
"name": "Substitute"
}
}
}
]
}
}
}

If we want to look up the next 10 items in the list, we just have to take the endCursor value in the pageInfo object and send it as the after argument for our next query:

query {
species(id: 1) {
moves(first: 10, after: "k") {
pageInfo {
hasNextPage
endCursor
}
edges {
cursor
node {
learned_at
move {
name
}
}
}
}
}
}

That will give us the next 10 items after the last item we saw from our previous query. Houdini takes care of most of this interaction automatically so you don’t need to worry too much about it. Just know that if you are writing a query against a field with connection-based pagination, it looks something like this:

query {
species(id: 1) {
moves {
edges {
node {
learned_at
}
}
}
}
}

For more information on the Connection model, check out this blog post.

Paginated Queries

Interacting with a paginated query is pretty similar to everything we’ve been doing so far. Just decorate the paginated field with the @paginate directive. Go ahead and change the +page.gql file to look like this:

src/routes/[[id]]/+page.gql
query Info($id: Int! = 1) {
species(id: $id) {
name
flavor_text
favorite
evolution_chain {
...SpeciesPreview
}
...SpriteInfo
moves(first: 1) @paginate {
edges {
node {
...MoveDisplay
}
}
}
}
favorites @list(name:"FavoriteSpecies") {
...FavoritePreview
}
}

At the surface, a paginated query store is basically the same thing as a normal query except for a few extra utilities. They’re pretty self-explanatory but just in case there’s any confusion: loadNextPage is an async function that will load the next page and append the result of the field tagged with @paginate to the existing value in our cache and there is a pageInfo value on the store that contains an object with meta data about the current page. For a more in-depth summary of what you can do with @paginate, you can check out the Pagination Guide.

It’s time to add some visuals. Add an import for the MoveDisplay component and copy the following block as the second child in the right panel (between div#species-evolution-chain and the nav):

src/routes/[[id]]/+page.svelte
<MoveDisplay move={$Info.data.species.moves.edges[0].node} />

Houdini supports a few different approachs for pagination but we won’t go too deep just yet. In our situation, since we want to display 1 move at a time, we need to pass SinglePage as the mode for @paginate:

src/routes/[[id]]/+page.gql
query Info($id: Int = 1) {
# ...
moves(first: 1) @paginate(mode: SinglePage) {
edges {
node {
...MoveDisplay
}
}
}
# ...
}
src/routes/[[id]]/+page.svelte
<script lang="ts">
import { UpButton, DownButton } from '~/components'
</script>
<!-- Use this instead of the move display we added earlier -->
<div id="move-summary">
<MoveDisplay move={$Info.data.species.moves.edges[0].node} />
<div id="move-controls">
<UpButton
disabled={!$Info.pageInfo.hasPreviousPage}
on:click={async () => await Info.loadPreviousPage()}
/>
<DownButton
disabled={!$Info.pageInfo.hasNextPage}
on:click={async () => await Info.loadNextPage()}
/>
</div>
</div>
src/routes/[[id]]/+page.svelte
<script>
import { UpButton, DownButton } from '~/components'
</script>
<!-- Use this instead of the move display we added earlier -->
<div id="move-summary">
<MoveDisplay move={$Info.data.species.moves.edges[0].node} />
<div id="move-controls">
<UpButton
disabled={!$Info.pageInfo.hasPreviousPage}
on:click={async () => await Info.loadPreviousPage()}
/>
<DownButton
disabled={!$Info.pageInfo.hasNextPage}
on:click={async () => await Info.loadNextPage()}
/>
</div>
</div>

You can now verify that it all works by opening the network tab and clicking on the up and down arrows. You should only see network requests being sent when loading a move we haven’t seen before.

That’s it!

This is the last topic we wanted to cover as part of the guide! Thank you so much for getting all the way through - we really appreciate the dedication. You can 🪄 share your achievement to help us! If there were any sections that were confusing, or changes you think would be helpful, please open up a discussion on GitHub.

If you want to read more about what Houdini can do, we recommending checking out the Working with GraphQL guide next.