Implementing Infinite scroll using NextJS, Prima, and React-Query

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 an Error we usually fetch out data here
  • This function also has access to an argument that has 2 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 string
  • Third argument has some options and one of them is getNextPageParams 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 load
  • isError 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 any
  • isFetchingNextPage 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 page
  • hasNextPage 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

useInView

  • This is a hook by react-intersection-observer package which is created on top of the native IntersectionObserver API of javascript
  • it returns 2 values
  • Firstly, ref which should be passed to any DOM node we want to observe
  • Secondly, 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

  • If the span element which we passed the ref 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 😂

63