Create Infinite Scroll component
The Infinite Scroll pattern allows you to automatically load more content when the user reaches the bottom of a list, without manually clicking a "Load More" button.
For that, we will build a InfiniteScroll
component that does the infinite logic and use the useIntersectionObserver hook.
Implementation
InfiniteScroll.tsx
import { ReactNode, useEffect } from 'react'
import { useIntersectionObserver } from './useIntersectionObserver'
type InfiniteScrollProps = {
children: ReactNode
hasNextPage: boolean
onLoadMore: () => void
threshold?: number
isLoading?: boolean // prevents duplicate fetches
}
export function InfiniteScroll({
children,
hasNextPage,
onLoadMore,
threshold = 100,
isLoading = false,
}: InfiniteScrollProps) {
const [containerRef, isIntersecting] = useIntersectionObserver({
rootMargin: `0px 0px ${threshold}px 0px`,
})
useEffect(() => {
if (isIntersecting && hasNextPage && !isLoading) {
onLoadMore()
}
}, [isIntersecting, hasNextPage, onLoadMore, isLoading])
return (
<div>
{children}
<div ref={containerRef} className="h-1" />
</div>
)
}
Usage
import { useState, useEffect, useCallback } from 'react'
import { InfiniteScroll } from './InfiniteScroll'
export default function PostsPage() {
const [posts, setPosts] = useState<any[]>([])
const [page, setPage] = useState(1)
const [hasNextPage, setHasNextPage] = useState(true)
const [loading, setLoading] = useState(false)
const loadMore = useCallback(async () => {
if (loading || !hasNextPage) return
setLoading(true)
const res = await fetch(`/api/posts?page=${page}`)
const data = await res.json()
setPosts(prev => [...prev, ...data.results])
setHasNextPage(data.results.length > 0)
setPage(prev => prev + 1)
setLoading(false)
}, [page, loading, hasNextPage])
// initial load
useEffect(() => {
loadMore()
}, [])
return (
<InfiniteScroll
hasNextPage={!!hasNextPage}
onLoadMore={fetchNextPage}
threshold={300} // prefetch earlier with 300 than 100
isLoading={loading} // avoid duplicate calls
>
{posts.map((post, index) => (
<div key={index} className="p-4 border-b">
<h2 className="font-bold">{post.title}</h2>
<p>{post.body}</p>
</div>
))}
{loading && <p>Loading...</p>}
</InfiniteScroll>
)
}
Usage with Tanstack InfinityQuery
import { useInfiniteQuery } from '@tanstack/react-query'
import { InfiniteScroll } from './InfiniteScroll'
type Post = {
id: number
title: string
body: string
}
async function fetchPosts({ pageParam = 1 }): Promise<{ results: Post[]; nextPage?: number }> {
const res = await fetch(`/api/posts?page=${pageParam}`)
if (!res.ok) throw new Error('Failed to fetch')
return res.json()
}
export default function PostsPage() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
getNextPageParam: lastPage => lastPage.nextPage ?? undefined,
})
return (
<InfiniteScroll hasNextPage={!!hasNextPage} onLoadMore={fetchNextPage} threshold={100} isLoading={isFetchingNextPage}>
{data?.pages.flatMap(page =>
page.results.map(post => (
<div key={post.id} className="p-4 border-b">
<h2 className="font-bold">{post.title}</h2>
<p>{post.body}</p>
</div>
))
)}
{isFetchingNextPage && <p>Loading...</p>}
</InfiniteScroll>
)
}