Implementing Pagination with Next.js

Pagination is the process of dividing items into multiple pages or manageable chunks. When using the List endpoint in our API, the number of items to retrieve from the request using the Take parameter defaults to 10. The number of items to retrieve from the request can be increased to a maximum of 50, however, any more than this will require the use of our Skip parameter which can be used for implementing Pagination.

In this tutorial, we'll be showing you how you can implement Pagination in Next.js on a Content List with the Skip and Take parameters using a Load More button to fetch more results.

Set up Content

In this tutorial, we will be paging through a list of Contacts. Our Contact Content Model consists of the following fields:

Name => Text Field

Position => Text Field

Image => Image Field

Set up Page Module

We've set up a Page Module with a Linked Content Field that links to our Contact Content List. We will use this Page Module to display our first 4 Contacts, then dynamically load the rest.

Lets Code

Getting Initial Contacts

In the code for our Contacts Page Module (Contacts.js), we can use the getCustomInitialProps function to call the Agility API and take 4 initial contacts at build time. We will return our 4 contacts and the totalCount for our Contacts Content List, which in this case is 10.

Contacts.getCustomInitialProps = async ({
  agility,
  languageCode,
}) => {

  const api = agility

  try {

    const contacts = await api.getContentList({
      referenceName: 'contacts',
      languageCode,
      take: 4,
      sort: 'properties.itemOrder',
      direction: api.types.SortDirections.ASC
    })

    return {
      contacts: contacts.items,
      totalCount: contacts.totalCount
    }

  } catch (err) {
    console.log(err)
  }
}

Updating Contacts Component

Next, let's add some code for our React Component. We can destructure customData from the component's props to further destructure  contacts and totalCount that we returned from our getCustomInitialProps function.

We can map over our contacts to display our initial results.

import React, { useState } from 'react'
import { AgilityImage } from "@agility/nextjs"

const Contacts = ({ customData }) => {

  const { contacts, totalCount } = customData;

  const [skip, setSkip] = useState(4)
  const [take, setTake] = useState(4);
  const [loading, setLoading] = useState(false)
  const [loadedContacts, setLoadedContacts] = useState([])

  const loadMoreContacts = async () => {
    setLoading(true)
    setTimeout(() => {
      setLoading(false)
    }, 500)
  }

  return (
    <div className="max-w-screen-xl mx-auto">
      <div className="grid grid-cols-4 gap-8">
        {contacts.map((contact, index) => (
          <div key={index}>
            <div className='relative h-64'>
              <AgilityImage
                src={contact.fields.image.url}
                alt={contact.fields.image.label}
                className="object-cover object-center rounded-lg"
                layout="fill"
              />
            </div>
            <p className='text-xl mt-2 font-bold'>{contact.fields.name}</p>
            <span className='text-gray-600'>{contact.fields.position}</span>
          </div>
        ))}
      </div>
      <div className='text-center mt-20'>
          <button onClick={loadMoreContacts} className="bg-primary-400 text-white p-4 rounded-lg w-40 mx-auto text-center">
            {loading ? 'Loading...' : 'Load More'}
          </button>
        </div>
    </div>
  )
}

export default Contacts

We can also set up some state for skiptakeloading, and our loadedContacts.

  • We set our skip to 4 as we'll want to skip 4 items each time
  • We set our take to 4 as we'll want to take 4 items each time
  • We set a loading state to show some feedback to the user
  • We set loadedContacts to an empty array

Update loadMoreContacts Function

Lets update our loadMoreContacts function with the following code:

  const loadMoreContacts = async () => {
    setLoading(true)
    setSkip(skip + 4)
    setTimeout(async () => {
      const { data } = await axios.get(`/api/getContacts?skip=${skip}&take=${take}`)
      setLoadedContacts(prevState => [...prevState, ...data.contacts])
      setLoading(false)
    }, 500)
  }
  • We set loading to true to show some feedback to the user
  • We set skip to skip + 4, this ensure's that we are always skipping the previous 4 contacts when fetching more
  • We set a timeout of 500ms that fetches contacts from our /api/getContacts API endpoint, passing in the skip and take values as query parameters
  • We set loading back to false.

Creating Our API Endpoint

We've created a file in the /pages/api directory called getContacts.js with the following code:

import agility from "@agility/content-fetch"

export default async (req, res) => {

    const { skip, take } = req.query

    const api = agility.getApi({
        guid: process.env.AGILITY_GUID,
        apiKey: process.env.AGILITY_API_FETCH_KEY
    })

    const contacts = await api.getContentList({
        referenceName: 'contacts',
        languageCode: 'en-us',
        take: take,
        skip: skip
    })

  res.status(200).json({ contacts: contacts.items })
};

This sets up an instance of our API using the @agility/content-fetch package. We grab skip & take from the request query parameters and use them to page through our API call and return our contacts.

Displaying Loaded Contacts

The final step is to map over the loadedContacts array to display them on the front end. Here's the complete code for our Contacts.js Page Module:

import React, { useState } from 'react'
import { AgilityImage } from "@agility/nextjs"
import axios from "axios"

const Contacts = ({ customData }) => {

  const { contacts, totalCount } = customData;

  const [skip, setSkip] = useState(4)
  const [take, setTake] = useState(4);
  const [loading, setLoading] = useState(false)
  const [loadedContacts, setLoadedContacts] = useState([])

  const loadMoreContacts = async () => {
    setLoading(true)
    setSkip(skip + 4)
    setTimeout(async () => {
      const { data } = await axios.get(`/api/getContacts?skip=${skip}&take=${take}`)
      setLoadedContacts(prevState => [...prevState, ...data.contacts])
      setLoading(false)
    }, 500)
  }

  return (
    <div className="max-w-screen-xl mx-auto">
      <div className="grid grid-cols-4 gap-8">
        {contacts.map((contact, index) => (
          <div key={index}>
            <div className='relative h-64'>
              <AgilityImage
                src={contact.fields.image.url}
                alt={contact.fields.image.label}
                className="object-cover object-center rounded-lg"
                layout="fill"
              />
            </div>
            <p className='text-xl mt-2 font-bold'>{contact.fields.name}</p>
            <span className='text-gray-600'>{contact.fields.position}</span>
          </div>
        ))}
        {loadedContacts && loadedContacts.map((contact, index) => (
          <div key={index}>
            <div className='relative h-64'>
              <AgilityImage
                src={contact.fields.image.url}
                alt={contact.fields.image.label}
                className="object-cover object-center rounded-lg"
                layout="fill"
              />
            </div>
            <p className='text-xl mt-2 font-bold'>{contact.fields.name}</p>
            <span className='text-gray-600'>{contact.fields.position}</span>
          </div>
        ))}
      </div>
      {totalCount - 4 !== loadedContacts.length && (
        <div className='text-center mt-20'>
          <button onClick={loadMoreContacts} className="bg-primary-400 text-white p-4 rounded-lg w-40 mx-auto text-center">
            {loading ? 'Loading...' : 'Load More'}
          </button>
        </div>
      )}
    </div>
  )
}

export default Contacts

Contacts.getCustomInitialProps = async ({
  agility,
  languageCode,
}) => {

  const api = agility

  try {

    const contacts = await api.getContentList({
      referenceName: 'contacts',
      languageCode,
      take: 4,
      sort: 'properties.itemOrder',
      direction: api.types.SortDirections.ASC
    })

    return {
      contacts: contacts.items,
      totalCount: contacts.totalCount
    }

  } catch (err) {
    console.log(err)
  }
}