In this post, I’ll walk you through creating an infinite scroll component. Whether you’re new to this concept or just need a refresher, we’ll break it down step by step.
This approach was instrumental in developing my product, TubeMemo, where users can efficiently scroll through and manage YouTube video notes without ever feeling overwhelmed by too much information at once.
Overview: What We’re Building
We’re going to create a component that loads a list of blog posts from a server.
We’ll use the useSWRInfinite
hook for data fetching and useInView
to detect when the user reaches the bottom of the list.
Setting Up the Component StructureOur component will handle:
State Management: We’ll track whether we’re in selection mode, which items are selected, and whether the infinite scroll has finished.
Fetching Data: We’ll use
useSWRInfinite
to handle loading paginated data from the server.Rendering Items: The posts will be displayed in a grid, and new posts will load as the user scrolls.
Scroll Detection:
useInView
will help us detect when the user has scrolled to the bottom of the list.
Key Parts of the Code Explained
Let's break down the key parts of the code so you can understand how it all works together.
- State and Hook Initialization:
const { ref, inView } = useInView();
const { userId } = useAuth();
const [finished, setFinished] = useState(false);
We use useInView
to know when a specific element (a placeholder div at the bottom of the list) is visible on the screen. The finished
state tracks whether we've loaded all available data.
-
Data Fetching with
useSWRInfinite
:
const getKey = (pageIndex: number, previousPageData: any) => {
if (previousPageData && !previousPageData.length) {
setFinished(true);
return null;
}
const offset = pageIndex === 0 ? null : previousPageData?.[previousPageData.length - 1]?.id;
return `/api/posts?offset=${offset}&limit=10`;
};
The getKey
function generates parameters for each API request. If there’s no more data (i.e., previousPageData
is empty), it stops further requests by returning null
.
The fetcher
function is responsible for making the API call:
interface Post {
id: string;
content: string;
}
const fetcher = async (url: string): Promise<Post[]> => {
const response = await fetch(url);
return response.json();
};
- Handling Scroll Events:
useEffect(() => {
if (inView && !isLoading && !isValidating && !finished) {
setSize((prevSize) => prevSize + 1);
}
}, [inView, isLoading, isValidating, setSize]);
This useEffect
hook checks if the bottom of the list is in view (inView
). If it is, and we’re not currently loading or validating data, and we haven’t finished loading all posts, we increment the page size to load more data.
Rendering the Content
The posts are rendered in a grid, and as new data is fetched, it’s appended to the list. We also have a loading state indicator that shows a placeholder while new data is being fetched:
return (
<div className="relative pt-4 w-full h-full space-y-2">
<h2 className="text-xl font-semibold">Your posts</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-2 md:gap-4">
{items?.map((item: any) => (
<BlogPost key={item?.id} data={item} />
))}
{isLoading && isValidating ? (
<div ref={ref}> Loading... </div>
) : null}
</div>
</div>
);
Putting it all together
import React, { useState, useEffect } from 'react';
import useSWRInfinite from 'swr/infinite';
import { useInView } from '@react-intersection-observer';
const fetcher = async (url: string) => {
const response = await fetch(url);
if (!response.ok) {
throw new Error('Network error');
}
return response.json();
};
const InfinityScrollList: React.FC = () => {
const { ref, inView } = useInView();
const [finished, setFinished] = useState(false);
const getKey = (pageIndex: number, previousPageData: any) => {
if (previousPageData && !previousPageData.length) {
setFinished(true);
return null;
}
const offset = pageIndex === 0 ? null : previousPageData?.[previousPageData.length - 1]?.id;
return `/api/posts?offset=${offset}&limit=8`;
};
const { data, error, size, setSize, isValidating } = useSWRInfinite(getKey, fetcher);
useEffect(() => {
if (inView && !isValidating && !finished) {
setSize((prevSize) => prevSize + 1);
}
}, [inView, isValidating, finished, setSize]);
const posts = data?.flat();
return (
<div className="relative pt-4 w-full h-full space-y-2">
<h2 className="text-xl font-semibold">Your posts</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-2 md:gap-4">
{posts?.map?.((post: any) => (
<div key={post.id} className="bg-gray-200 p-4 rounded-lg">
<h3 className="text-lg font-bold">{post.title}</h3>
<p>{post.content}</p>
</div>
))}
{(isValidating && size > 1) && (
<div ref={ref}> Loading... </div>
)}
</div>
</div>
);
};
export default InfinityScrollList;
Conclusion
Infinite scrolling can greatly enhance user engagement, especially in content-heavy applications. With this guide, you should be well-equipped to implement it in your projects.
Top comments (0)