00
:
00
:
00
:
00
Corso SEO AI - Usa SEOEMAIL al checkout per il 30% di sconto

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 undefined se 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
    },
  })
}