[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
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.
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
).
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
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.
// ...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()
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
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
$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; } }}
- [Next.js]: Using environment(env) variables in Next.js
- [Next.js]: Implementing infinite scroll in Next.js