74
Implementing Infinite scroll using NextJS, Prima, and React-Query
Hello everyone, in this article lets see how can we build an infinite scroll UI pattern using NextJs, Prisma, and React-Query

TLDR: Link to code
TTLDR: Link to video
Open an empty folder in your preferred editor and create a NextJS project by typing
npx create-next-app . --ts
in the command line of that project. This will create a NextJS project with typescript in the current folder, now let's install some dependencies npm install @prisma/client axios react-intersection-observer react-query
npm install -D prisma faker @types/faker
Open a terminal in the root directory and type
npx prisma init
this will initialize a Prisma project by creating a folder named prisma
having schema.prisma
file in it and in the root directory we can see a .env
file with DATABASE_URL
environment variable which is a connection string to the database, in this article we will use postgres, so database URL should look something this."postgresql://<USER>:<PASSWORD>@localhost:5432/<DATABASE>?schema=public"
Change the connection URL according to your configuration(make sure you do this part without any typos if not Prisma will not be able to connect to the database)
open
schema.prisma
file and paste the below code which is a basic model for a Post
model Post {
id Int @id @default(autoincrement())
title String
createdAt DateTime @default(now())
}
This in itself will not create
Post
table in out database we have to migrate the changes by using the following commandnpx prisma migrate dev --name=init
This will create
Post
table in the database specified (if there is an error in connection URL this step will fail, make sure you have no typos in DATABASE_URL
) and generates types for us to work with.Create a file
seed.js
in prisma
directory and lets write a seed script to fill out database with some fake dataconst { PrismaClient } = require('@prisma/client')
const { lorem } = require('faker')
const prisma = new PrismaClient()
const seed = async () => {
const postPromises = []
new Array(50).fill(0).forEach((_) => {
postPromises.push(
prisma.post.create({
data: {
title: lorem.sentence(),
},
})
)
})
const posts = await Promise.all(postPromises)
console.log(posts)
}
seed()
.catch((err) => {
console.error(err)
process.exit(1)
})
.finally(async () => {
await prisma.$disconnect()
})
Add the below key-value pair to
package.json
"prisma": {
"seed": "node ./prisma/seed.js"
}
Then run
npx prisma db seed
this will run seed.js
file we will have 50
posts in ourdatabase which is quite enough to implement infinite scrollLets now write an API route so that we can get our posts, create a file
post.ts
inside /pages/api
import type { NextApiRequest, NextApiResponse } from 'next'
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
type Post = {
id: number
title: string
createdAt: Date
}
interface Data {
posts: Post[]
nextId: number | undefined
}
export default async (req: NextApiRequest, res: NextApiResponse<Data>) => {
if (req.method === 'GET') {
const limit = 5
const cursor = req.query.cursor ?? ''
const cursorObj = cursor === '' ? undefined : { id: parseInt(cursor as string, 10) }
const posts = await prisma.post.findMany({
skip: cursor !== '' ? 1 : 0,
cursor: cursorObj,
take: limit,
})
return res.json({ posts, nextId: posts.length === limit ? posts[limit - 1].id : undefined })
}
}
The above API route on a
GET
request checks for a query parameter cursor
if cursor
is empty we just return limit
number of posts, but if the cursor is not empty we skip
one post and send limit
posts, along with posts we also send nextId
which will be used by React-Query to send further requests In
index.tsx
of pages
directory use the code belowimport React, { useEffect } from 'react'
import { useInfiniteQuery } from 'react-query'
import axios from 'axios'
import { useInView } from 'react-intersection-observer'
export default function Home() {
const { ref, inView } = useInView()
const { isLoading, isError, data, error, isFetchingNextPage, fetchNextPage, hasNextPage } =
useInfiniteQuery(
'posts',
async ({ pageParam = '' }) => {
await new Promise((res) => setTimeout(res, 1000))
const res = await axios.get('/api/post?cursor=' + pageParam)
return res.data
},
{
getNextPageParam: (lastPage) => lastPage.nextId ?? false,
}
)
useEffect(() => {
if (inView && hasNextPage) {
fetchNextPage()
}
}, [inView])
if (isLoading) return <div className="loading">Loading...</div>
if (isError) return <div>Error! {JSON.stringify(error)}</div>
return (
<div className="container">
{data &&
data.pages.map((page) => {
return (
<React.Fragment key={page.nextId ?? 'lastPage'}>
{page.posts.map((post: { id: number; title: string; createdAt: Date }) => (
<div className="post" key={post.id}>
<p>{post.id}</p>
<p>{post.title}</p>
<p>{post.createdAt}</p>
</div>
))}
</React.Fragment>
)
})}
{isFetchingNextPage ? <div className="loading">Loading...</div> : null}
<span style={{ visibility: 'hidden' }} ref={ref}>
intersection observer marker
</span>
</div>
)
}
Let's understand what's happening here
Promise
or throws an Error
we usually fetch out data here2
properties namely queryKey
which is the first argument of useInfiniteQuery
and pageParams
which is returned by getNextPageParams
and initially its undefined
hence we are setting its default value as an empty stringgetNextPageParams
which should return some value that will be passed as pageParams
to the next request isLoading
is a boolean
that indicates the status of query on first loadisError
is a boolean
which is true
if there is any error thrown by the query function(second argument of useInfiniteQuery
)data
is the result of the successful request and contains data.pages
which is the actual data from the request and pageParams
error
has the information about the error if there is anyisFetchingNextPage
is a boolean
which can be used to know the fetching state of the request fetchNextPage
is the actual function that is responsible to fetch the data for the next pagehasNextPage
is a boolean
that says if there is a next page to be fetched, always returns true
until the return value from getNextPageParams
is undefnied
react-intersection-observer
package which is created on top of the native IntersectionObserver
API of javascript 2
values ref
which should be passed to any DOM node we want to observe
inView
which is a boolean
that is true
if the node that we set to observe
is in the viewport Then we use a
useEffect
hook to check 2 conditions span
element which we passed the ref
is in the viewport or not.If both the conditions satisfy we then fetch the next page, that's it, this is all it takes to build an infinite scroll UI pattern
I hope you found some value in the article, make sure you check the full code here as I did not include any code to style our beautiful posts 😂
74