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' });
}
};
- 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;
}
- 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
- 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>
)
}
- 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)
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.
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.
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?
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.
Nices
Some comments may only be visible to logged-in visitors. Sign in to view all comments.