[Next.js]: Implementing infinite scroll in Next.js
Next.js
05/12/2020
Overview
This post will explain how to build infinite scrolling with server side rendering in Next.js.
Install packages
Unless you're using Next.js @^9.4, you need to install node-fetch
npm i node-fetch
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 an infinite scroll, 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 30 users per load (perPage
) along with current page (curPage
) and maximum Page (maxPage
).
const getUsers = async (req, res, next) => { const curPage = req.query.page || 1 // Display 30 users per page load const perPage = 30
try { // Fetch users based on current page * perPage const users = await User.find().limit(perPage * curPage) 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 scrolling to the last user triggers fetch for the new page with handleScroll
import React, { useState, useEffect } from "react"import { useRouter } from "next/router"
import "./styles.scss"
const UserList = ({ userData }) => { const [users, setUsers] = useState([]) const router = useRouter()
// Set users from userData useEffect(() => { if (userData) { if (userData.error) { // Handle error } else { setUsers(userData.users) } } }, [userData])
// Listen to scroll positions for loading more data on scroll useEffect(() => { window.addEventListener("scroll", handleScroll) return () => { window.removeEventListener("scroll", handleScroll) } })
const handleScroll = () => { // To get page offset of last user const lastUserLoaded = document.querySelector( ".user-list > .user:last-child" ) if (lastUserLoaded) { const lastUserLoadedOffset = lastUserLoaded.offsetTop + lastUserLoaded.clientHeight const pageOffset = window.pageYOffset + window.innerHeight // Detects when user scrolls down till the last user if (pageOffset > lastUserLoadedOffset) { // Stops loading if (userData.curPage < userData.maxPage) { // Trigger fetch const query = router.query query.page = parseInt(userData.curPage) + 1 router.push({ pathname: router.pathname, 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> </> )}
export default UserList
Adding loader between page navigation
Currently, scrolling down to the last user may trigger fetch multiple times!. You'll need to add a Router
event to detect whether the fetch has ended to prevent multiple fetches.
import React, { useState, useEffect } from "react"import Router, { useRouter } from "next/router"
import "./styles.scss"
const UserList = ({ userData }) => { const [users, setUsers] = useState([]) const router = useRouter() const [loading, setLoading] = useState(false) const startLoading = () => setLoading(true) const stopLoading = () => setLoading(false)
// Set up user data useEffect(() => { if (userData) { // Error check if (userData.error) { // Handle error } else { setUsers(userData.users) } } }, [userData])
// Router event handler useEffect(() => { Router.events.on("routeChangeStart", startLoading) Router.events.on("routeChangeComplete", stopLoading) return () => { Router.events.off("routeChangeStart", startLoading) Router.events.off("routeChangeComplete", stopLoading) } }, [])
// Listen to scroll positions for loading more data on scroll useEffect(() => { window.addEventListener("scroll", handleScroll) return () => { window.removeEventListener("scroll", handleScroll) } })
const handleScroll = () => { // To get page offset of last user const lastUserLoaded = document.querySelector( ".user-list > .user:last-child" ) if (lastUserLoaded) { const lastUserLoadedOffset = lastUserLoaded.offsetTop + lastUserLoaded.clientHeight const pageOffset = window.pageYOffset + window.innerHeight
if (pageOffset > lastUserLoadedOffset) { // Stops loading /* IMPORTANT: Add !loading */ if (userData.curPage < userData.maxPage && !loading) { // Trigger fetch const query = router.query query.page = parseInt(userData.curPage) + 1 router.push({ pathname: router.pathname, 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> {loading && <h1>Loading ...</h1>} </> )}
export default UserList
Extra: Styling for user
.user { padding: 1rem;}
.user-list { list-style: none; display: flex; justify-content: center; flex-wrap: wrap; .user { padding: 2rem; margin: 1rem; background: #ccc; }}
- [Next.js]: Implementing pagination in Next.js
- [SSH]: Remote SSH to a Raspberry Pi without password (macOS, Linux)