[Next.js]: Implementing pagination in Next.js

Next.js

05/11/2020


Overview

This post will explain how to build pagination with server side rendering in Next.js.

Install packages

Unless you're using Next.js @^9.4, you need to install node-fetch, and we'll use react-paginate for pagination component

BASH
npm i node-fetch react-paginate

getServerSideProps()

We define getServerSideProps() that is not much different from a regular fetch function, but the difference is that the server generates the page HTML upon each request, and it can exist only inside a page component.

Since we're building a pagination, we're not going to fetch the whole data at once, but instead, we'll request a partial data from the server by using query parameter that contains which page to navigate to. It will later be passed from UserList component.

/pages/index.js
JS
import fetch from "node-fetch"
import UserList from "../components/UserList"
const HomePage = ({ userData }) => {
return (
<div className="home-page">
<UserList userData={userData} />
</div>
)
}
export const getServerSideProps = async ({ query }) => {
// Fetch the first page as default
const page = query.page || 1
let userData = null
// Fetch data from external API
try {
const res = await fetch(`${process.env.FETCH_URL}/users?page=${page}`)
if (res.status !== 200) {
throw new Error("Failed to fetch")
}
userData = await res.json()
} catch (err) {
userData = { error: { message: err.message } }
}
// Pass data to the page via props
return { props: { userData } }
}
export default HomePage

Backend API

Let's take a look at server side API for retrieving user. It returns 10 users (defined in perPage) along with current page (curPage) and maximum Page (maxPage).

JS
const getUsers = async (req, res, next) => {
const curPage = req.query.page || 1
// Display 10 users per page
const perPage = 10
try {
// Fetch users based on current page * perPage
const users = await User.find()
.skip((curPage - 1) * perPage)
.limit(perPage)
const totalUsers = await User.find().countDocuments()
res.status(200).json({
message: "Fetched users",
users: users,
curPage: curPage,
maxPage: Math.ceil(totalUsers / perPage),
})
} catch (err) { // ... }
}

UserList component

UserList renders list of users and pagination component which triggers fetch for the new page with handlePagination

/components/UserList/index.js
JSX
import React, { useState, useEffect } from "react"
import ReactPaginate from "react-paginate"
import { useRouter } from "next/router"
import "./styles.scss"
const UserList = ({ userData }) => {
const [users, setUsers] = useState([])
const router = useRouter()
useEffect(() => {
if (userData) {
if (userData.error) {
// Handle error
} else {
// Set users from userData
setUsers(userData.users)
}
}
}, [userData])
// Triggers fetch for new page
const handlePagination = page => {
const path = router.pathname
const query = router.query
query.page = page.selected + 1
router.push({
pathname: path,
query: query,
})
}
return (
<>
<ul className="user-list">
{users.length > 0 &&
users.map((user, i) => {
return (
<li className="user" key={i}>
<span>{user.name}</span>
</li>
)
})}
</ul>
<ReactPaginate
marginPagesDisplayed={2}
pageRangeDisplayed={5}
previousLabel={"previous"}
nextLabel={"next"}
breakLabel={"..."}
initialPage={userData.curPage - 1}
pageCount={userData.maxPage}
onPageChange={handlePagination}
/>
</>
)
}
export default UserList

Adding loader between page navigation

Since navigating to different page requires re-fetch, you can add your custom loader with Router events.

/components/UserList/index.js
JSX
// ...
import Router, { useRouter } from "next/router"
const UserList = ({ userData }) => {
const [loading, setLoading] = useState(false)
const startLoading = () => setLoading(true)
const stopLoading = () => setLoading(false)
useEffect(() => {
// Router event handler
Router.events.on("routeChangeStart", startLoading)
Router.events.on("routeChangeComplete", stopLoading)
return () => {
Router.events.off("routeChangeStart", startLoading)
Router.events.off("routeChangeComplete", stopLoading)
}
}, [])
// ...
return (
<>
{loading && <h1>Loading..</h1>}
{/* ... */}
</>
)
}
export default UserList

scrollIntoView()

If your pagination component is placed below a long user list, scrolling up after clicking a page may be more user friendly. You can use useRef to assign a ref to a DOM element to scroll up with scrollIntoView()

/components/UserList/index.js
JSX
import React, { useState, useEffect, useRef } from "react"
// ...
const UserList = ({ userData }) => {
const userListRef = useRef(null)
// ...
const handlePagination = page => {
const path = router.pathname
const query = router.query
query.page = page.selected + 1
router.push({
pathname: path,
query: query,
})
userListRef.current.scrollIntoView()
}
return (
<>
<ul className="user-list" ref={userListRef}>
{loading && <h1>Loading ...</h1>}
{users.length > 0 &&
users.map((user, i) => {
return (
<li className="user" key={i}>
<span>{user.name}</span>
</li>
)
})}
</ul>
{/* ... */}
</>
)
}
export default UserList

Extra: Styling for user & paginate component

/components/UserList/index.js
JSX
import "./styles.scss"
const UserList = ({ userData }) => {
// ....
return (
<>
{/* ... */}
<ReactPaginate
marginPagesDisplayed={2}
pageRangeDisplayed={5}
previousLabel={"previous"}
nextLabel={"next"}
breakLabel={"..."}
initialPage={userData.curPage - 1}
pageCount={userData.maxPage}
onPageChange={handlePagination}
containerClassName={"paginate-wrap"}
subContainerClassName={"paginate-inner"}
pageClassName={"paginate-li"}
pageLinkClassName={"paginate-a"}
activeClassName={"paginate-active"}
nextLinkClassName={"paginate-next-a"}
previousLinkClassName={"paginate-prev-a"}
breakLinkClassName={"paginate-break-a"}
/>
</>
)
}
export default UserList
/components/UserList/styles.scss
SCSS
$purple-color: #745788;
$purple-color-lighter: #d9c5e6;
.user {
padding: 1rem;
}
.user-list {
list-style: none;
display: flex;
justify-content: center;
flex-wrap: wrap;
.user {
padding: 2rem;
margin: 1rem;
background: #ccc;
}
}
// Pagination styling
.paginate-wrap {
display: flex;
align-items: center;
list-style: none;
margin: 1rem 0 0 0;
padding: 0.5rem 1rem;
border-radius: 3px;
max-width: 100%;
flex-wrap: wrap;
background: #ccc;
body.light & {
background: #fafafa;
}
.paginate-a,
.paginate-next-a,
.paginate-prev-a,
.paginate-break-a {
cursor: pointer;
padding: 0.2rem 0.4rem;
&:focus {
outline: 0;
}
&:hover {
background: $purple-color;
background: $purple-color-lighter;
}
}
.paginate-li {
margin: 0 0.2rem;
}
.paginate-next-a,
.paginate-prev-a {
color: $purple-color;
margin: 0 0.3rem;
}
.paginate-active {
border: 2px solid $purple-color-lighter;
background: $purple-color-lighter;
}
.paginate-disabled > a {
cursor: not-allowed;
background: transparent;
color: #666;
body.light & {
color: #ccc;
}
&:hover {
background: transparent !important;
}
}
}

WRITTEN BY

Keeping a record