Web Studio is here! An enhanced experience to make it easier to create, preview, and collaborate on your website contentLearn More
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 skip
, take
, loading
, 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
toskip
+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 theskip
andtake
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)
}
}