How To Add Simple Pagination To Your Next.js 13 App

Pagination is a common feature in web applications that allows users to navigate through a large dataset by dividing it into smaller, more manageable chunks. In Next.js 13, pagination can be implemented using a combination of server-side rendering and client-side logic. To simplify the process we’ll be using Jotai to manage the state across different components and Next.js Pages, to save the pagination progress of the user.

insidetech
7 min readDec 23, 2022

Note: since Next.js 13 is in beta right now, the code described here might not work in the future

Photo by Arnold Francisca on Unsplash

Setting up the project and fetching our data

To begin, you’ll need to install Next.js 13 and set up a new project.

Make sure you are using the experimental appDir by setting it to true, if you are reading this while Next.js 13 is still in beta. I might update this, in the future, if this isn’t necessary anymore.

We also need to install Jotai with npm. Just run npm i jotai in the root folder of your Next.js project and you are good to go.

Once you have a basic Next.js app with Jotai up and running, we can start by fetching our blogposts by using fetch(). It is the new simplified way in Next.js 13 to fetch data in your app. Depending on how the data you are retrieving is structured, you may need to do a little trial and error, although I gave my best to make this tutorial universally applicable.

// app/page.jsx

/* here we fetch our blog posts and pass them as props to
the BlogPostList Component */

export async function getPosts() {

const res = await fetch('https://api.example.com/...');
return res.json();

}

/* note that we use async/await here,
which was not possible in previous versions of next.js */
export default async function Home() {

const posts = await getPosts();

return (
<div></div>
)
}

getServerSideProps, getStaticProps, getInitialProps are not supported in the new Next.js 13 app directory anymore, therefore we are using fetch()

As soon as we receive our posts from our Endpoint in our page, like in the code below, we need to create a new functional component and import it in our page.jsx. I will call it BlogPostList.jsx and pass the posts as the posts-prop.

// app/page.jsx

import BlogPostList from '../components/BlogPostList'

/* here we fetch our blog posts and pass them as props to
the BlogPostList Component */

export async function getPosts() {

const res = await fetch('https://api.example.com/...');
return res.json();

}

/* note that we use async/await here,
which was not possible in previous versions of next.js */
export default async function Home() {

const posts = await getPosts();

return (
<div>
<BlogPostList posts={posts}/>
</div>

)
}

If you got your data from the API you are using and passed it to the BlogPostList.jsx component you can destructure the props in the function declaration and log them, to see if everything is working.

Now you should have access to your list of posts in the BlogPostList.jsx-Component.

Keep in mind that your console.logs will be logged on the server, when you want to log them from page.jsx, so you will not see them in your browser. Unless you are using a client component, you should check the terminal in which you started the dev server to see if your log contains the data you need.

Implementing the slice() and map() methods and setting up the route-directories

Note that our BlogPostList.jsx contains the string ‘use client’ at the top of the code. This allows us to use standard react hooks like for example useState() and event handlers like onClick in our JSX, since they will only work on the client. If we would try to use those methods on a Next.js server component they wouldn’t work.

// components/BlogPostList.jsx

'use client'

import React from 'react'
import Link from 'next/link'

export default function BlogPostList({ posts }) {

return (
<>
<div>
{/* we will specify the values of the slice params in the next steps */}
{posts.slice(currentSliceStart, currentSliceEnd).map((posts) => (
<div key={posts.id}>
<Link href={`/blog/${posts.id}`}>
<h1>{posts.postTitle}</h1>
<p>{posts.previewText}</p>
</Link>
</div>
))}
</div>
{/* button loads two more posts on load posts and disappears if no more posts can be loaded */}
{currentSliceEnd < posts.items.length && <button onClick={nextPage}>Load more posts</button>}
</>
)
}

To paginate our blog posts later we are using the slice()-method on the posts array. We will pass two stateful variables to the method: currentSliceStart and currentSliceEnd. They will specify which range of posts we want to display in our component. The attached map()-method will then render only the specified posts that are within the slice range of the page. By using Next.js’ Link we are able to dynamically create links to the different posts-subpages. Make sure to import Link at the top of the component from next/link.

To make the dynamic routes work, we need to create two new directories inside the app component. The blog-directory, and the [post]-directory. Notice how the [post]-directory is wrapped in curly braces. This means that it is a dynamic route. For every id mapped in our BlogPostList a new route will be created.

Adding state with Jotai and providing Page-Buttons

First we need to create a storage-directory in our root in which we will create the atoms.js file.

In there we will create our state by using so called atoms.

// storage/atom.js

import { atom } from 'jotai';

const sliceStartAtom = atom(0)
const sliceEndAtom = atom(4)
const currentPageAtom = atom(1)

export {
sliceStartAtom,
sliceEndAtom,
currentPageAtom
};

Thats it. We can use those values in every client component throughout the whole project.

To make use of the atoms we will import them into our BlogPostList.jsx and the use the custom useAtom() -Hook. useAtom() works similar to useState(), but it can get and set the values from everywhere in our app. It’s basically a useState() for global state-setting.

// components/BlogPostList.jsx

'use client'

import React from 'react'
import Image from 'next/image'
import Link from 'next/link'

import { sliceStartAtom, sliceEndAtom, currentPageAtom } from '../storage/atoms'
import { useAtom } from 'jotai'

To go to the next page in our blog list we will just add to the currentSliceStart and currentSliceEnd to specify a new range of posts that should be mapped out. We are doing this by calling the nextPage()-function which just adds a specified integer to the current state by using the setter-Functions declared in the useAtom() Hooks. The previousPage() -function works like nextPage()-function, but subtracts from the current states.

// components/BlogPostList.jsx

...

// using the global state from Jotai for setting our slice values
const [currentSliceStart, setCurrentSliceStart] = useAtom(sliceStartAtom)
const [currentSliceEnd, setCurrentSliceEnd] = useAtom(sliceEndAtom)
const [currentPage, setCurrentPage] = useAtom(currentPageAtom)

// the number that is added to the states specifies how many posts are displayed per page
const nextPage = () => {
setCurrentSliceStart(currentSliceStart + 4)
setCurrentSliceEnd(currentSliceEnd + 4)
setCurrentPage(currentPage + 1)
}

const previousPage = () => {
setCurrentSliceStart(currentSliceStart - 4)
setCurrentSliceEnd(currentSliceEnd - 4)
setCurrentPage(currentPage - 1)
}

...

To avoid adding or subtracting too much from our atoms we’ll be just disabling the buttons that call nextPage() and previousPage() later, if they surpass certain values or potentially go below zero. We want to stay between zero and the length of our posts-array.

// components/BlogPostList.jsx

'use client'

import React from 'react'
import Image from 'next/image'
import Link from 'next/link'

import { sliceStartAtom, sliceEndAtom, currentPageAtom } from '../storage/atoms'
import { useAtom } from 'jotai'

export default function BlogPostList({ posts }) {

// using the global state from Jotai for setting our slice values
const [currentSliceStart, setCurrentSliceStart] = useAtom(sliceStartAtom)
const [currentSliceEnd, setCurrentSliceEnd] = useAtom(sliceEndAtom)
const [currentPage, setCurrentPage] = useAtom(currentPageAtom)

// the number that is added to the states specifies how many posts are displayed per page
const nextPage = () => {
setCurrentSliceStart(currentSliceStart + 4)
setCurrentSliceEnd(currentSliceEnd + 4)
setCurrentPage(currentPage + 1)
}

const previousPage = () => {
setCurrentSliceStart(currentSliceStart - 4)
setCurrentSliceEnd(currentSliceEnd - 4)
setCurrentPage(currentPage - 1)
}


return (
<>
<div>
{posts.slice(currentSliceStart, currentSliceEnd).map((posts) => (
<div key={posts.id}>
<Link href={`/blog/${posts.id}`}>
<h1>{posts.postTitle}</h1>
<p>{posts.previewText}</p>
</Link>
</div>
))}
</div>
{currentSliceStart >= 4 && <button onClick={previousPage}>previous</button>}
{currentSliceEnd < posts.items.length && <button onClick={nextPage}>next</button>}
</>
)
}

Thats it. Now you should be able to navigate trough your posts by clicking the previous and next-buttons as long as your posts-array has more than 4 items. If you want the steps to be smaller or bigger you can just adjust the numbers in the nextPage and previousPage functions and adjusting the conditions in front of the buttons in the jsx.

If you only use currentSliceEnd and add to it while hardcoding the start of the slice to 0 you can also continually load more posts in as you add to the state of currentSliceEnd instead of using paginations. If you add to setCurrentSliceEnd by implementing a scroll-function that fires if the user is near the end of the page, this could also be built out into a kind of infinite scroll functionality.

If you need help or have recommendations to make the code better, feel free to leave a comment or message me.

I can also provide access to a github-repository if needed that contains all the code and a fully built out blog-template as well that uses Contentful as CMS.

Blog is live, visit here

--

--