Introduction
Infinite scroll is a technique that allows for infinite scrolling of content on a website by loading and displaying more content as the user scrolls down a page. This creates a seamless and uninterrupted browsing experience as it helps prevent the hassle of navigating through pages just to view content. Facebook, Twitter and Pinterest are popularly known for using this technique to asynchronously fetch more posts as users scroll to the end of their timeline.
The live version for this tutorial is here
Target Audience
This tutorial is for beginner and intermediate NextJs developer who wants to learn how to implement infinite scroll with NextJs 13 server actions. Of course, you don’t have to be an expert to be able to follow along with this tutorial.
What is Server Action
According to the NextJs 13 docs, with Server Actions, you don't need to manually create API endpoints. Instead, you define asynchronous server functions that can be called directly from your components. Server Actions can be defined in Server Components or called from Client Components.
In simple terms, you can think of server action as an asynchronous javascript function that runs on the server. It is still an experimental alpha feature in NextJs 13 which you can enable in your next.config.js
file:
module.exports = {
experimental: {
serverActions: true,
},
};
Example
In this example, we are going to build an infinite scrolling news feed. This example uses NextJs 13, TypeScript, News Api (link here) and Tailwindcss. I will assume you already know how to install and run NextJs applications and without wasting much time, let’s jump straight into it.
You'll need to register on News Api website to get your own api key. You can also see the live version of the tutorial here
Moving forward, let’s create the following files in our Nextjs app directories:
- fetch-news.ts
- useInView.tsx
- LoadMoreArticle.jsx
If you have your api key, create a .env.local
at the root directory of your application and add your API key inside:
// .env.local
NEXT_PUBLIC_API_KEY = ***************;
At the end, our folder structure should look like this:
We will start implementing the core mechanism of our application by fetching data from our News API with NextJs 13 Server Action.
//fetch-news.ts
"use server";
export async function fetchNews({
limit,
offset,
}: {
limit: number,
offset: number,
}) {
const res = await fetch(
`https://newsapi.org/v2/top-headlines?country=us&category=technology&pageSize=${limit}&page=${offset}&apiKey=${process.env.NEXT_PUBLIC_API_KEY}`
);
if (!res.ok) {
throw new Error("Failed to fetch data");
}
const { articles, totalResults } = await res.json();
return { articles, totalResults };
}
Let's do a breakdown of the code above, we declared the use server
directive at the top of the file to let NextJs know that we are using server actions
. Then we proceed to define a server action that fetches news articles from a News API that allows us to customize the number of articles we want to retrieve (known as limit) and where we want to start in the result set (known as offset). We will also handle errors and return articles and totalResults
from the retrieved data.
At the background, NextJs will initiate an HTTP POST request to our endpoint using the fetch API. Even when our client calls the server action, the POST request is still being made on the server.
We already know that server Action can be defined in Server Components or called from Client Components. In our case, the server action will be used both in the server and client component hence why we define the server action in a separate file (
fetch-news.ts
) so that our client and server component can have access to it.
Next step is to invoke our server action and retrieve the article data from it in page.tsx
file
//page.tsx
import LoadMoreArticles from "./LoadMoreArticles";
import { fetchNews } from "./fetch-news";
import Link from "next/link";
export default async function Home() {
const { articles } = await fetchNews({
limit: 10,
offset: 1,
});
return (
<section className="max-w-4xl mx-auto">
<h1 className="text-3xl mb-10 text-center font-semibold">
Technology News
</h1>
{articles?.map((article: { title: string, url: string }, i: number) => {
return (
<Link
key={i}
href={article.url}
className="hover:underline"
target="_blank"
rel="noopener noreferrer"
>
<article className="flex items-center bg-white p-4 mb-4 rounded shadow">
<h2>{article.title}</h2>
</article>
</Link>
);
})}
<LoadMoreArticles />
</section>
);
}
We called the server action asynchronously with an object as an argument. The object has two properties:
- limit: 10: This sets the limit for the number of news articles to be retrieved to 10. It specifies that you want to fetch 10 articles.
- offset: 1: This sets the offset for where the retrieval should start to 1.
We then go on to display our first 10 news article that we retrieved from our server action
Implementing the infinite scroll
What we want to achieve now is that when we scroll to the end of the first 10 articles, we want to display a loading spinner to show that we are expecting more article content from our API.
So let’s render a loading spinner in LoadMoreArticles.jsx.
//LoadMoreArticle.tsx
import ClipLoader from "react-spinners/ClipLoader";
const LoadMoreArticles = () => {
return (
<div>
<div className="flex justify-center">
<ClipLoader
color={"#444"}
loading={true}
size={40}
aria-label="Loading Spinner"
/>
</div>
</div>
);
};
export default LoadMoreArticles;
This result shows a loading spinner at the end of the articles
Our next task is to request for more articles from the API when the loading spinner is visible on our screen.
But how do we detect the visibility of the loading spinner?
So in our useInView.tsx, we will create a custom hook that uses the Intersection Observer API to track whether the loading spinner is visible within the viewport or not
//useInView.tsx
"use client";
import React, { useState, useEffect, RefObject } from "react";
const useInView = (ref: RefObject<HTMLElement>) => {
const [isInView, setIsInView] = useState(false);
useEffect(() => {
if (ref?.current) {
const observer = new IntersectionObserver(([entry]) => {
setIsInView(entry.isIntersecting);
});
observer.observe(ref.current);
return () => observer.disconnect();
}
}, [ref]);
return { isInView };
};
export default useInView;
Let's breakdown the code above, we simply perform a check to see if our referenced element (loading spinner) exists in the DOM. If it does, we create a new IntersectionObserver
that tracks when our loading spinners enters or exits the viewport, we then update the isInView state variable based on the entry.isIntersecting value which will return a boolean value of true
if the loading spinner intersects with the intersection observer's root or false
if its not intersecting
Now that we can successfully track the visibility of our loading spinner, our next task is to request for more articles from our API when the loading spinner is visible, we then go on to display the results in our infinite scrolling component which is LoadMoreArticles.tsx
. This component will be responsible for displaying more article contents on our screen.
//LoadMoreArticles
"use client";
import { useEffect, useRef, useState } from "react";
import ClipLoader from "react-spinners/ClipLoader";
import useInView from "./useInView";
import { fetchNews } from "./fetch-news";
import Link from "next/link";
interface Article {
title: string;
url: string;
}
interface DataType {
total: number | null;
articles: Article[] | [];
}
const LoadMoreArticles = () => {
const container = useRef<HTMLDivElement | null>(null);
const { isInView } = useInView(container);
const [articleData, setArticleData] = useState<DataType>({
total: null,
articles: [],
});
const offset = (articleData?.articles?.length + 20) / 10;
const remainder = (articleData?.total as number) % 2;
useEffect(() => {
if (isInView) {
fetchNews({
limit: 10,
offset: offset,
}).then((res) => {
setArticleData((prevData) => ({
total: res?.totalResults,
articles: [...prevData.articles, ...res?.articles],
}));
});
}
}, [isInView]);
return (
<div>
{articleData.articles?.map((article, i) => {
return (
<Link
key={i}
href={article.url}
className="hover:underline"
target="_blank"
rel="noopener noreferrer"
>
<article className="flex items-center bg-white p-4 mb-4 rounded shadow">
<h2>{article.title}</h2>
</article>
</Link>
);
})}
{articleData.articles?.length - remainder !==
(articleData?.total as number) - 20 ? (
<div ref={container} className="flex justify-center">
<ClipLoader
color={"#444"}
loading={true}
size={40}
aria-label="Loading Spinner"
/>
</div>
) : (
""
)}
</div>
);
};
export default LoadMoreArticles;
let’s breakdown down the code, we simply retrieve 10 new articles from our server action by dynamically changing the offset
value each time the loading spinner is visible on our screen. At the end, we also hide our loading spinner when we have successfully retrieved all the articles from the API. Right now, we should have a perfect and working infinite scrolling news feed.
A quick look into the network tabs shows that we are indeed making our request from nextjs server action.
Conclusion
In this article, you’ve learned what server action is and how you can use it to build an infinite scrolling news feed. There is a whole lot to what server action can do apart from fetching data, you can check the NextJs docs to see other examples. The source for this tutorial can be found here on my github and you can also checkout the live version