Intro
Hello everyone, in this article lets see how can we build an infinite scroll UI pattern using NextJs, Prisma, and React-Query
Final result
TLDR: Link to code
TTLDR: Link to video
Project setup
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
Initializing Prisma
Open a terminal in the root directory and typenpx 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 command
npx 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.
Seeding database
Create a file seed.js
in prisma
directory and lets write a seed script to fill out database with some fake data
const { 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 scroll
Creating API route
Lets 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
Using useInfiniteQuery
In index.tsx
of pages
directory use the code below
import 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
useInfiniteQuery
- It takes 3 arguments
- first is the unique key, which is required by react-query to use internally for caching and many other things
- A function that returns a
Promise
or throws anError
we usually fetch out data here - This function also has access to an argument that has
2
properties namelyqueryKey
which is the first argument ofuseInfiniteQuery
andpageParams
which is returned bygetNextPageParams
and initially itsundefined
hence we are setting its default value as an empty string - Third argument has some options and one of them is
getNextPageParams
which should return some value that will be passed aspageParams
to the next request -
isLoading
is aboolean
that indicates the status of query on first load -
isError
is aboolean
which istrue
if there is any error thrown by the query function(second argument ofuseInfiniteQuery
) -
data
is the result of the successful request and containsdata.pages
which is the actual data from the request andpageParams
-
error
has the information about the error if there is any -
isFetchingNextPage
is aboolean
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 page -
hasNextPage
is aboolean
that says if there is a next page to be fetched, always returnstrue
until the return value fromgetNextPageParams
isundefnied
useInView
- This is a hook by
react-intersection-observer
package which is created on top of the nativeIntersectionObserver
API of javascript - it returns
2
values - Firstly,
ref
which should be passed to any DOM node we want toobserve
- Secondly,
inView
which is aboolean
that istrue
if the node that we set toobserve
is in the viewport
Then we use a useEffect
hook to check 2 conditions
- If the
span
element which we passed theref
is in the viewport or not. - If we have any data to fetch 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
Outro
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 😂
Top comments (1)
I was trying to implement the same thing and stumbled across your article. Good demo!