[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-paginategetServerSideProps()
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 HomePageBackend 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 UserListAdding 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 UserListscrollIntoView()
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 UserListExtra: 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