TL;DR: TanStack Querys
useInfiniteQuerybewältigt Cursor- und Offset-basierte Pagination mit integriertem State-Management für Seitenansammlung, Richtung und Prefetching. Dieser Leitfaden behandelt Cursor-basierte Pagination mit TanStack Start Server Functions, Reverse Pagination für Chat-ähnliche UIs, Stale-While-Revalidate für Infinite Lists und Optimistic Prepending für Echtzeit-Updates.
Einleitung
Pagination ist überraschend komplex. Offset-basierte Pagination funktioniert nicht mehr, wenn Elemente hinzugefügt oder entfernt werden. Cursor-basierte Pagination erfordert sorgfältiges State-Management. Infinite Scroll bringt Edge Cases beim Prefetching, Race Conditions und Speicherwachstum mit sich.
TanStack Querys useInfiniteQuery abstrahiert den Großteil dieser Komplexität. Es verwaltet die Seitenansammlung, stellt fetchNextPage- und fetchPreviousPage-Callbacks bereit und integriert sich nahtlos in TanStack Routers Preloading-System.
Cursor-Pagination mit Server Functions
import { createServerFn } from '@tanstack/react-start'
import { z } from 'zod'
import { db } from '../db'
export const listProductsFn = createServerFn({ method: 'GET' })
.validator(z.object({
cursor: z.string().optional(),
limit: z.number().default(20),
}))
.handler(async ({ data }) => {
const { cursor, limit } = data
const products = await db
.selectFrom('products')
.orderBy('createdAt', 'desc')
.limit(limit + 1)
.cursor(cursor ? { after: cursor } : undefined)
.execute()
const hasMore = products.length > limit
const items = hasMore ? products.slice(0, limit) : products
const nextCursor = hasMore ? items[items.length - 1].id : null
return { items, nextCursor }
})
Der useInfiniteQuery-Hook
import { useInfiniteQuery } from '@tanstack/react-query'
import { listProductsFn } from '../server/products'
function ProductList() {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
useInfiniteQuery({
queryKey: ['products'],
queryFn: ({ pageParam }) =>
listProductsFn({ data: { cursor: pageParam } }),
initialPageParam: undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
return (
<div>
{data?.pages.map((page) =>
page.items.map((product) => <ProductCard key={product.id} product={product} />)
)}
{hasNextPage && (
<button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
{isFetchingNextPage ? 'Loading...' : 'Load More'}
</button>
)}
</div>
)
}
Infinite Scroll mit Intersection Observer
function InfiniteProductList() {
const observerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
fetchNextPage()
}
},
{ threshold: 0.1 }
)
if (observerRef.current) observer.observe(observerRef.current)
return () => observer.disconnect()
}, [fetchNextPage, hasNextPage, isFetchingNextPage])
return (
<div>
{items}
<div ref={observerRef} />
</div>
)
}
Vergleich der Pagination-Strategien
| Strategie | Vorteile | Nachteile | Optimal für |
|---|---|---|---|
| Offset (LIMIT/OFFSET) | Einfach, direkt zu Seite N springen | Funktioniert nicht bei Einfügungen/Löschungen | Statische Datensätze |
| Cursor (Keyset) | Stabil, schnell bei großen Datenmengen | Seiten nicht überspringbar | Dynamische Feeds |
| Zeitbasiert | Natürliche Sortierung | Ungleiche Seitengrößen | Aktivitätsströme |
| Search-after (Elastic) | Funktioniert mit Volltextsuche | DB-spezifisch | Suchergebnisse |
Fazit
useInfiniteQuery mit Cursor-basierter Pagination bietet die beste UX für dynamische Datenfeeds. In Kombination mit TanStack Start Server Functions erhalten Sie typsichere Pagination von der Datenbank bis zur UI – mit minimalem Boilerplate.
Für serverseitige Pagination-Muster empfehlen wir TanStack Query: Server Functions Best Practices. Meistern Sie das Cache-Management mit Cache Invalidation Strategies. Wenn Sie große paginierte Listen rendern, lässt sich TanStack Virtual Scrolling natürlich mit Infinite Queries kombinieren, und der Leitfaden SaaS Database Architecture behandelt die zugrunde liegenden Datenstrukturen für effiziente Pagination.