DEV Community

Cover image for From Server to Client: Handling Initial Data Fetching and Infinite Scroll
Shubham Tiwari
Shubham Tiwari

Posted on

From Server to Client: Handling Initial Data Fetching and Infinite Scroll

Hello my frontend developers, today i will be sharing a pattern of data fetching and want all of yours suggestions about this and if there is any better way to do this.

Tech Stack - Next JS (frontend) + Express and MongoDB (backend)

Feature:

So i was implementing a feature where the list of data will have infinite scrolling but with with a load more button at the end of the list, showing more data and loading limited data initially

Implementation:

I have loaded the initial data with a limit as page 1 initially on server side to reduce load on the client side, then using client side and useEffect, created infinite scrolling with a load more button which loads more data as click it until we reach the last item in the data and hiding the load more button.

Backend setup

import { Exercises } from '../../schemas/Exercise.js';
import { Request, Response } from 'express';

export const getExercises = async (req: Request, res: Response) => {
    const { user, limit, page } = req.body;

    if (!user)
        return res.status(400).json({ message: 'Bad Request - user is required' });

    try {
        const hasMoreExercises = await Exercises.countDocuments({ "user.email": user.email }) > limit * page;
        const exercises = await Exercises.find({ "user.email": user.email }).limit(limit).skip((page - 1) * limit).sort({ date: -1 });
        if (!exercises || exercises.length === 0) {
            return res.status(404).json({ message: 'No exercises found for this user' });
        }
        res.status(200).json({ exercises, hasMoreExercises });
    } catch (err) {
        console.error(err);
        res.status(500).json({ message: 'Internal Server Error' });
    }
};
Enter fullscreen mode Exit fullscreen mode
  • Firstly we are extracting out the required data from req.body
  • Then adding a conditional check for the user object
  • hasMoreExercises checks if there is more data in the database by checking whether the limit times page is larger than the documents count and return this boolean flag to the client to perform data fetching for the next page until it reached the end and returns false.
  • Using limit and skip, we send the data in chunks (limit = 50, page = 2 will fetch the data from 51 - 100)
  • Sorting with date as descendening to show latest data first
  • In the end, returning the data as well as hasMoreExercises flag.

Frontend (Data Fetching util)

// utils.ts
export const fetchExercises = async ({ user, page }: { user: User | undefined, page: number }) => {
  const response = await fetch("http://localhost:5000/exercises/get", {
    method: "POST",
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ user, limit: 50, page }),
  });
  const data = await response.json();
  return data;
}
Enter fullscreen mode Exit fullscreen mode
  • This method just takes user and page as params and fetch the data from the api using these params value dynamically.
  • Using body property, we are passing the required params to the api and perform the data fetching accordingly.

Frontend (Server side fetching)

import { AddExerciseForm } from '@/components/exercise/form/AddExercise'
import ExerciseList from '@/components/exercise/exercise-list/ExerciseList'
import React from 'react'
import { auth } from '../api/auth/nextAuth'
import { redirect } from 'next/navigation'
import { fetchExercises } from '@/lib/utils'

const page = async () => {
    const session = await auth();
    const exercises = await fetchExercises({ user: session?.user, page: 1 });

    if (!session?.user) {
        redirect("/");
    }
    return (
        <main className="font-sans min-h-screen bg-slate-950 text-slate-100 pt-10">
            <div className="max-w-7xl mx-auto px-5 py-20">
                <div className='flex flex-wrap justify-between gap-5 items-center mb-10'>
                    <h1 className="text-3xl font-bold text-center">Exercise Tracker</h1>
                    <AddExerciseForm />
                </div>
                <ExerciseList exercisesData={exercises} user={session?.user} />
            </div>
        </main>
    )
}

export default page
Enter fullscreen mode Exit fullscreen mode
  • Here we are using the same fetchExercises util with user from next-auth library and page as 1 since it is initial load and we have to show the first set of data.

Frontend (Client side fetching - infinite scrolling)

"use client"
import { ExerciseFormValues, useExerciseStoreList } from "@/store/exercise"
import { useEffect, useState } from "react"
import { Button } from "@/components/ui/button"
import { User as NextAuthUser } from "next-auth"
import { fetchExercises } from "@/lib/utils"

export default function ExerciseList({ exercisesData, user }: { exercisesData: { exercises: ExerciseFormValues[], hasMoreExercises: boolean }, user: NextAuthUser | undefined }) {
    const { exercises, setExercises } = useExerciseStoreList((state) => state)
    const [page, setPage] = useState(1)
    const [hasMoreExercises, setHasMoreExercises] = useState(exercisesData.hasMoreExercises)

    useEffect(() => {
        setExercises(exercisesData.exercises)
    }, [])
    return (
        <section className="grid grid-cols-1 gap-10">

           {/* UI Mapping of data */}

            <div className="flex justify-center mt-5">
                {
                    hasMoreExercises ? (
                        <Button variant="outline" size="default" className="cursor-pointer w-fit text-primary" onClick={() => {
                            fetchExercises({ user: user, page: page + 1 }).then((data) => {
                                if (data && data.exercises.length > 0 && exercises) {
                                    setExercises([...exercises, ...data.exercises])
                                    setHasMoreExercises(data.hasMoreExercises)
                                }
                            })
                            setPage(page + 1)
                        }}>Load more...</Button>
                    ) : null
                }
            </div>
        </section>
    )
}
Enter fullscreen mode Exit fullscreen mode
  • Here we are loading the states from zustand which is a state management library
  • useEffect will set the initial state from server side fetching to this component
  • Load more button will again call the api but with increment of page by 1, so if its page 1, the load more will call the api with page 2 loading next set of data and setting the state by appending the new set of data to the existing set of data.

That's it for this post, Let me know if i could do any improvements or follow a more robust and efficient approach for this feature.

You can contact me on -

Instagram - https://www.instagram.com/supremacism__shubh/
LinkedIn - https://www.linkedin.com/in/shubham-tiwari-b7544b193/
Email - shubhmtiwri00@gmail.com

You can help me with some donation at the link below Thank you👇👇
https://www.buymeacoffee.com/waaduheck

Also check these posts as well

Top comments (6)

Collapse
 
roshan_650272631d16fcdaae profile image
Roshan

1.Use query params instead of req.body for GET-like requests
Pagination (limit, page) fits more naturally as query parameters:
GET /exercises?userEmail=...&page=1&limit=50
This is also more cache-friendly (good for CDNs).

2.Add indexes on MongoDB
Since you’re filtering by user.email and sorting by date, make sure you have a compound index like:
db.exercises.createIndex({ "user.email": 1, date: -1 })
This makes pagination queries much faster.

  1. Switch from skip/limit to cursor-based pagination skip becomes slower on large collections. Instead, you can paginate using the last item’s _id or date. Example: Exercises.find({ "user.email": user.email, date: { $lt: lastDate } }) .limit(limit) .sort({ date: -1 }) This avoids scanning skipped documents.

4.Use React Query or SWR instead of manual fetch + state
They handle caching, deduplication, error retries, and infinite queries out of the box. Example with React Query’s useInfiniteQuery:
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery(
["exercises", user?.email],
({ pageParam = 1 }) => fetchExercises({ user, page: pageParam }),
{ getNextPageParam: (lastPage, pages) => lastPage.hasMoreExercises ? pages.length + 1 : undefined }
)
This simplifies your Load more logic.

5.SSR + Hydration edge case
When using server-side fetching (page.tsx), you’re fetching with fetchExercises (which calls your backend API). In Next.js, you could instead fetch directly from the DB on the server (through an internal server action or RSC) to avoid an extra HTTP hop for page 1.

Collapse
 
shubhamtiwari909 profile image
Shubham Tiwari
  1. Using query params are fine but we can't expose the user email like that in the url, also it depends on the type of app we have, in this case we won't share the url with anyone since it's a dashboard based project so it will be accessed by user itself
  2. Already added index for user email, it's a good feature
  3. I am using skip because I want to load the data in chunks of let's say 50, with your example I am not sure how It skips data based on the limit
  4. For this type of simple fetching, it's fine to use native fetch instead of adding additional overhead of library and increase bundle size. Also I am using zustand for state management so, it becomes another headache to integrate it with React query
  5. Yeah but next js api route has very limited features and need libraries to handle features like rate-limiting, CSP. I am using express for its extensive features and support. Next JS is good but can't handle a robust API in its ecosystem that too with organized structure and reusability
Collapse
 
shubhamtiwari909 profile image
Shubham Tiwari

Also I want to know about the overall approach for data fetching whether is it fine combining server side and client side like this, give your thoughts?

Collapse
 
theunkn_c0822c29c6da profile image
The unknow

I am looking for a beginner or amateur developer to share my ideas for creating companies with new concepts adapted to the future and artificial intelligence.
Please write to me or reply to my comment so we can get started.
You never know, we might be the next Jeff Bezos in 20 years.

Collapse
 
werliton profile image
Werliton Silva

Nices

Some comments may only be visible to logged-in visitors. Sign in to view all comments.