How to do infinite scrolling in Next.js with Server Components and Server Actions
With the advent of React Server Components (RSCs) and Server Actions, many new capabilities that were previously difficult to achieve in React, either requiring third-party libraries or a significant amount of boilerplate, are now achievable out of the box. One such capability is infinite scrolling.
In this tutorial, we will see how to implement infinite scrolling in Next.js leveraging Server Components and Server Actions, along with the Intersection Observer browser API.
We are going to build an infinite scrolling list of items with random data that I generated using an LLM. The core idea is to utilize React’s newest features to fetch data when the user scrolls to the bottom of the page just like many social media platforms do.
Info
First ever blog post alert! 🎉 Please be
nice haha. Feedback welcome, but go easy on me!
Prerequisites
If you’re not familiar with RSCs and Server Actions, I recommend reading the official documentation of RSCs and Server Actions to better understand their functionality and use cases.
Let’s get started!
You can simply read through this tutorial to understand how it works, or you can follow along and build the feature as we go. I’ll guide you through the process of creating a Next.js app that implements an infinite scrolling list of items from scratch.
First, we need to bootstrap a Next.js app that is compatible with React 19, as of now, we can do so by running the following command:
Create a Post Component
This is just a quick and simple Post component, there’s not so much story to it.
In a real-world scenario, this would be more complex, including features like buttons and author avatars. But for this tutorial, we’ll keep it simple.
Adding Mock Data
Below is just mock data created with an LLM to simulate a database, again not so much story to it.
Creating a Component to Display all Posts
Now we’re getting to the juicy stuff!
This component will render all the posts and invoke some 🪄magic✨ to fetch the following posts when the user reaches the bottom of the page.
Step 1. Because this component will interact
with a Browser API, it must be a Client Component. To make this component run on
the client let’s add the “use client” directive at the top of the
file. Otherwise, it would run only on the server, and since the server evidently
cannot access the Browser APIs it wouldn’t do anything.
Step 2. Let’s use the Intersection Observer API
to detect when the user reaches the bottom of the page.
We could code down the intersection observer, but I prefer just to use a third-party package, just to not deviate ourselves from the topic. So we’re going to install a library that contains useful hooks that performs common tasks:
I won’t explain in depth what an Intersection Observer is, but in a nutshell it allows us to detect when an element is visible in the viewport, and we can set a threshold to determine when it is visible. If you’d like to learn more about it, I suggest you reading this article: JavaScript Intersection Observer Ultimate Guide
Step 4. Adding State to Hold the Posts
For the moment the posts state will be an empty array, but later we’ll fetch the posts from the server.
The Interesting Part Begins Here! 👀
Let’s create a server action that will fetch posts from the mock data we created earlier.
Step 1. Create a new file and add the “use server” directive at the top of the
file. This will tell the bundler that all the code inside this file will run
only on the server, so when you import any function from this file, it will not
be included in the client bundle, and in place put a endpoint call.
Then, import the mock data we created earlier.
Step 2. Now, when it comes to infinite scroll,
we need to fetch the next piece of data, so we need to add a parameter to the
function that will be the offset of the data we want to fetch. This is just one
way to do it, you can do it in many other ways, for example, in a real world
scenario you would fetch the data from a database and cursor-based infinite
scrolling might be better suited for this case, but for the sake of this
tutorial we will keep it simple.
Step 3. Now we need to slice the data from the
mock data array.
Step 4. Finally, we need to map over the new
posts and return something that we can use to render in the Posts client
component. We could just return an array of objects, something like:
But, to make it more interesting, we can return a component, be it a Client Component, a Server Component or just a <div> for example. In our case, we will return and object that has an array of the Post components we created earlier which is a Server Component, and a boolean that tells us if there are more posts to fetch.
Fetching Initial Posts
Before we move on to fetching more posts as the user scrolls down, let’s fetch the initial posts when the component first renders.
For this we need to call the getPosts function we created earlier in the app/page.tsx file, which is the entry point of our app.
We can set an initial offset, for example, 5, and fetch the first 5 posts.
Then we pass both the initial posts and the initial offset to the Posts component, so that when the component first renders it will show the initial posts and when the user reaches the bottom of the page it knows from where to start fetching more posts.
Now, let’s go back to the Posts component and render the initial posts.
Fetching More Posts
Let’s call the server action to get more posts when the user reaches the bottom of the page:
What we just did is to call the getPosts function when the user reaches the bottom of the page, and then set the new posts to the state.
Handling Pending States
Now, if we want to implement pending states, I found that we can use the useActionState hook, as it allows you to do it out of the box very easily, but this hook has one big caveat, and it is that when the action is called (the returned action that useActionState returns), it will send the current state to the server, so if we have a lot of posts, it will send all of them to the server, which is not ideal. So for this case, I wouldn’t recommend using the useActionState hook.
Fortunately we still have a way to handle the pending state, and that is by using the useTransition hook.
And there you have it! A simple implementation of infinite scrolling leveraging Server Components and Server Actions of React. You can build upon this basic framework to create more complex and robust infinite scrolling mechanisms.