import { useInfiniteQuery } from '@tanstack/react-query'
import api from '@/services/api'
import { Post, PaginatedResponse } from '@/types'
export function useInfinitePosts() {
return useInfiniteQuery({
queryKey: ['posts', 'infinite'],
queryFn: async ({ pageParam = 1 }) => {
const { data } = await api.get<PaginatedResponse<Post>>('/posts', {
params: { page: pageParam },
})
return data
},
getNextPageParam: (lastPage) => {
const { current_page, total_pages } = lastPage.meta
return current_page < total_pages ? current_page + 1 : undefined
},
initialPageParam: 1,
})
}
import { useEffect, useRef } from 'react'
import { useInfinitePosts } from '@/hooks/useInfinitePosts'
import { PostCard } from './PostCard'
export function PostsList() {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfinitePosts()
const sentinelRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
fetchNextPage()
}
},
{ rootMargin: '100px' }
)
if (sentinelRef.current) {
observer.observe(sentinelRef.current)
}
return () => observer.disconnect()
}, [hasNextPage, isFetchingNextPage, fetchNextPage])
const posts = data?.pages.flatMap((page) => page.data) ?? []
return (
<div className="space-y-6">
{posts.map((post) => (
<PostCard key={post.id} post={post} />
))}
{hasNextPage && (
<div ref={sentinelRef} className="py-8 text-center">
{isFetchingNextPage ? (
<div className="spinner"></div>
) : (
<button
onClick={() => fetchNextPage()}
className="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
>
Load More
</button>
)}
</div>
)}
</div>
)
}
Infinite scroll loads more content as users reach the bottom of a list, improving perceived performance over pagination. I use the Intersection Observer API to detect when a sentinel element becomes visible, then trigger the next page fetch. React Query's useInfiniteQuery manages pagination state and combines pages into a flattened list. The getNextPageParam function extracts pagination metadata from the API response to determine if more pages exist. Each new page appends to the existing data array. I also provide a 'Load More' button as a fallback for users who prefer manual control or when JavaScript fails. This pattern works great for feeds, search results, or any long lists.