Infinite Queries
useInfiniteQuery
useInfiniteQuery e’ progettato per gestire dati paginati dove le pagine si accumulano (scroll infinito, “Carica di piu’”, ecc.).
import { useInfiniteQuery } from '@tanstack/react-query'
interface PostsResponse {
posts: Post[]
nextCursor: number | null
}
function usePosts() {
return useInfiniteQuery({
queryKey: ['posts'],
queryFn: async ({ pageParam }): Promise<PostsResponse> => {
const res = await fetch(`/api/posts?cursor=${pageParam}`)
return res.json()
},
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
}
Parametri Fondamentali
- initialPageParam: Il valore iniziale per il primo fetch.
- getNextPageParam: Funzione che riceve l’ultima pagina e ritorna il parametro per la pagina successiva, oppure
undefinedse non ci sono altre pagine. - getPreviousPageParam: Opzionale, per la paginazione bidirezionale.
Implementare lo Scroll Infinito
Ecco un componente completo con scroll infinito:
import { useInfiniteQuery } from '@tanstack/react-query'
import { useEffect, useRef } from 'react'
interface Post {
id: number
title: string
body: string
}
interface PostsPage {
data: Post[]
nextPage: number | null
totalPages: number
}
function InfinitePostList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isPending,
isError,
error,
} = useInfiniteQuery({
queryKey: ['posts', 'infinite'],
queryFn: async ({ pageParam }): Promise<PostsPage> => {
const res = await fetch(`/api/posts?page=${pageParam}&limit=20`)
if (!res.ok) throw new Error('Errore nel caricamento')
return res.json()
},
initialPageParam: 1,
getNextPageParam: (lastPage) => lastPage.nextPage,
})
// Ref per l'elemento sentinella
const loadMoreRef = useRef<HTMLDivElement>(null)
// Intersection Observer per il caricamento automatico
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
fetchNextPage()
}
},
{ threshold: 0.1 }
)
if (loadMoreRef.current) {
observer.observe(loadMoreRef.current)
}
return () => observer.disconnect()
}, [fetchNextPage, hasNextPage, isFetchingNextPage])
if (isPending) return <p>Caricamento...</p>
if (isError) return <p>Errore: {error.message}</p>
return (
<div>
{data.pages.map((page, pageIndex) => (
<div key={pageIndex}>
{page.data.map((post) => (
<article key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</article>
))}
</div>
))}
{/* Elemento sentinella per l'auto-loading */}
<div ref={loadMoreRef}>
{isFetchingNextPage ? (
<p>Caricamento altri post...</p>
) : hasNextPage ? (
<p>Scorri per caricare altri post</p>
) : (
<p>Hai raggiunto la fine</p>
)}
</div>
</div>
)
}
Bottone “Carica di Piu’”
Se preferisci un approccio manuale al posto dell’auto-loading:
function PostListWithButton() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: fetchPostsPage,
initialPageParam: 1,
getNextPageParam: (lastPage) => lastPage.nextPage,
})
// Appiattisci tutte le pagine in un unico array
const allPosts = data?.pages.flatMap((page) => page.data) ?? []
return (
<div>
{allPosts.map((post) => (
<PostCard key={post.id} post={post} />
))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage
? 'Caricamento...'
: hasNextPage
? 'Carica altri'
: 'Nessun altro risultato'}
</button>
</div>
)
}
Paginazione Bidirezionale
Per caricare pagine sia avanti che indietro (es. una chat o un feed centrato su un elemento):
function BidirectionalFeed({ startCursor }: { startCursor: number }) {
const {
data,
fetchNextPage,
fetchPreviousPage,
hasNextPage,
hasPreviousPage,
isFetchingNextPage,
isFetchingPreviousPage,
} = useInfiniteQuery({
queryKey: ['feed', startCursor],
queryFn: async ({ pageParam }) => {
const res = await fetch(`/api/feed?cursor=${pageParam}&limit=20`)
return res.json()
},
initialPageParam: startCursor,
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
getPreviousPageParam: (firstPage) => firstPage.prevCursor ?? undefined,
})
return (
<div>
{hasPreviousPage && (
<button
onClick={() => fetchPreviousPage()}
disabled={isFetchingPreviousPage}
>
{isFetchingPreviousPage ? 'Caricamento...' : 'Carica precedenti'}
</button>
)}
{data?.pages.map((page, i) => (
<div key={i}>
{page.items.map((item: any) => (
<FeedItem key={item.id} item={item} />
))}
</div>
))}
{hasNextPage && (
<button
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage}
>
{isFetchingNextPage ? 'Caricamento...' : 'Carica successivi'}
</button>
)}
</div>
)
}
Paginazione Basata su Offset
Non tutti i backend usano cursori. Ecco come gestire offset e limit classici:
function usePaginatedItems(limit: number = 20) {
return useInfiniteQuery({
queryKey: ['items', { limit }],
queryFn: async ({ pageParam }) => {
const res = await fetch(`/api/items?offset=${pageParam}&limit=${limit}`)
return res.json() as Promise<{ items: Item[]; total: number }>
},
initialPageParam: 0,
getNextPageParam: (lastPage, allPages) => {
const totalFetched = allPages.reduce((acc, p) => acc + p.items.length, 0)
return totalFetched < lastPage.total ? totalFetched : undefined
},
})
}