Das Rendern von Listen, die Daten additiv zu einem bestehenden Datensatz "hinzufügen" oder "unendlich scrollen" können, ist ebenfalls ein sehr gängiges UI-Muster. TanStack Query unterstützt eine nützliche Version von useQuery namens useInfiniteQuery für das Abfragen dieser Arten von Listen.
Bei der Verwendung von useInfiniteQuery werden Sie einige Unterschiede feststellen
Hinweis: Die Optionen initialData oder placeholderData müssen dieselbe Struktur aufweisen wie ein Objekt mit den Eigenschaften data.pages und data.pageParams.
Nehmen wir an, wir haben eine API, die Projekte seitenweise, 3 auf einmal, basierend auf einem Cursor-Index zurückgibt, zusammen mit einem Cursor, der zum Abrufen der nächsten Gruppe von Projekten verwendet werden kann
fetch('/api/projects?cursor=0')
// { data: [...], nextCursor: 3}
fetch('/api/projects?cursor=3')
// { data: [...], nextCursor: 6}
fetch('/api/projects?cursor=6')
// { data: [...], nextCursor: 9}
fetch('/api/projects?cursor=9')
// { data: [...] }
fetch('/api/projects?cursor=0')
// { data: [...], nextCursor: 3}
fetch('/api/projects?cursor=3')
// { data: [...], nextCursor: 6}
fetch('/api/projects?cursor=6')
// { data: [...], nextCursor: 9}
fetch('/api/projects?cursor=9')
// { data: [...] }
Mit diesen Informationen können wir eine "Mehr laden"-UI erstellen, indem wir
import { useInfiniteQuery } from '@tanstack/react-query'
function Projects() {
const fetchProjects = async ({ pageParam }) => {
const res = await fetch('/api/projects?cursor=' + pageParam)
return res.json()
}
const {
data,
error,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
status,
} = useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
})
return status === 'pending' ? (
<p>Loading...</p>
) : status === 'error' ? (
<p>Error: {error.message}</p>
) : (
<>
{data.pages.map((group, i) => (
<React.Fragment key={i}>
{group.data.map((project) => (
<p key={project.id}>{project.name}</p>
))}
</React.Fragment>
))}
<div>
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetching}
>
{isFetchingNextPage
? 'Loading more...'
: hasNextPage
? 'Load More'
: 'Nothing more to load'}
</button>
</div>
<div>{isFetching && !isFetchingNextPage ? 'Fetching...' : null}</div>
</>
)
}
import { useInfiniteQuery } from '@tanstack/react-query'
function Projects() {
const fetchProjects = async ({ pageParam }) => {
const res = await fetch('/api/projects?cursor=' + pageParam)
return res.json()
}
const {
data,
error,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
status,
} = useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
})
return status === 'pending' ? (
<p>Loading...</p>
) : status === 'error' ? (
<p>Error: {error.message}</p>
) : (
<>
{data.pages.map((group, i) => (
<React.Fragment key={i}>
{group.data.map((project) => (
<p key={project.id}>{project.name}</p>
))}
</React.Fragment>
))}
<div>
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetching}
>
{isFetchingNextPage
? 'Loading more...'
: hasNextPage
? 'Load More'
: 'Nothing more to load'}
</button>
</div>
<div>{isFetching && !isFetchingNextPage ? 'Fetching...' : null}</div>
</>
)
}
Es ist wichtig zu verstehen, dass der Aufruf von fetchNextPage, während ein laufender Abruf stattfindet, das Risiko birgt, Hintergrundaktualisierungen von Daten zu überschreiben. Diese Situation wird besonders kritisch, wenn eine Liste gerendert und fetchNextPage gleichzeitig ausgelöst wird.
Denken Sie daran, dass es für eine InfiniteQuery nur einen laufenden Abruf geben kann. Ein einzelner Cache-Eintrag wird für alle Seiten gemeinsam genutzt. Der Versuch, zweimal gleichzeitig abzurufen, kann zu Datenüberschreibungen führen.
Wenn Sie gleichzeitiges Abrufen aktivieren möchten, können Sie die Option { cancelRefetch: false } (Standard: true) innerhalb von fetchNextPage verwenden.
Um einen nahtlosen Abfrageprozess ohne Konflikte zu gewährleisten, wird dringend empfohlen, zu überprüfen, ob die Abfrage sich nicht im isFetching-Zustand befindet, insbesondere wenn der Benutzer diesen Aufruf nicht direkt steuert.
<List onEndReached={() => hasNextPage && !isFetching && fetchNextPage()} />
<List onEndReached={() => hasNextPage && !isFetching && fetchNextPage()} />
Wenn eine unendliche Abfrage veraltet wird und erneut abgerufen werden muss, werden die einzelnen Gruppen sequenziell abgerufen, beginnend mit der ersten. Dies stellt sicher, dass wir, selbst wenn die zugrunde liegenden Daten geändert wurden, keine veralteten Cursor verwenden und möglicherweise Duplikate erhalten oder Datensätze überspringen. Wenn die Ergebnisse einer unendlichen Abfrage jemals aus dem QueryCache entfernt werden, beginnt die Paginierung im anfänglichen Zustand mit nur der anfänglichen Gruppe, die angefordert wird.
Bidirektionale Listen können durch die Verwendung der Eigenschaften und Funktionen getPreviousPageParam, fetchPreviousPage, hasPreviousPage und isFetchingPreviousPage implementiert werden.
useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
})
useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
})
Manchmal möchten Sie die Seiten in umgekehrter Reihenfolge anzeigen. In diesem Fall können Sie die Option select verwenden
useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
select: (data) => ({
pages: [...data.pages].reverse(),
pageParams: [...data.pageParams].reverse(),
}),
})
useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
select: (data) => ({
pages: [...data.pages].reverse(),
pageParams: [...data.pageParams].reverse(),
}),
})
queryClient.setQueryData(['projects'], (data) => ({
pages: data.pages.slice(1),
pageParams: data.pageParams.slice(1),
}))
queryClient.setQueryData(['projects'], (data) => ({
pages: data.pages.slice(1),
pageParams: data.pageParams.slice(1),
}))
const newPagesArray =
oldPagesArray?.pages.map((page) =>
page.filter((val) => val.id !== updatedId),
) ?? []
queryClient.setQueryData(['projects'], (data) => ({
pages: newPagesArray,
pageParams: data.pageParams,
}))
const newPagesArray =
oldPagesArray?.pages.map((page) =>
page.filter((val) => val.id !== updatedId),
) ?? []
queryClient.setQueryData(['projects'], (data) => ({
pages: newPagesArray,
pageParams: data.pageParams,
}))
queryClient.setQueryData(['projects'], (data) => ({
pages: data.pages.slice(0, 1),
pageParams: data.pageParams.slice(0, 1),
}))
queryClient.setQueryData(['projects'], (data) => ({
pages: data.pages.slice(0, 1),
pageParams: data.pageParams.slice(0, 1),
}))
Stellen Sie sicher, dass die Datenstruktur von Seiten und Seitenparametern immer gleich bleibt!
In einigen Anwendungsfällen möchten Sie möglicherweise die Anzahl der im Abfrage-Daten gespeicherten Seiten begrenzen, um die Leistung und die Benutzerfreundlichkeit zu verbessern
Die Lösung besteht darin, eine "begrenzte unendliche Abfrage" zu verwenden. Dies wird durch die Verwendung der Option maxPages in Verbindung mit getNextPageParam und getPreviousPageParam ermöglicht, um das Abrufen von Seiten bei Bedarf in beide Richtungen zu ermöglichen.
Im folgenden Beispiel werden nur 3 Seiten im Array der Abfrage-Daten-Seiten beibehalten. Wenn ein erneuter Abruf erforderlich ist, werden nur 3 Seiten sequenziell erneut abgerufen.
useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
maxPages: 3,
})
useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
maxPages: 3,
})
Wenn Ihre API keinen Cursor zurückgibt, können Sie den pageParam als Cursor verwenden. Da getNextPageParam und getPreviousPageParam auch den pageParam der aktuellen Seite erhalten, können Sie diesen zur Berechnung des nächsten / vorherigen Seitenparameters verwenden.
return useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage, allPages, lastPageParam) => {
if (lastPage.length === 0) {
return undefined
}
return lastPageParam + 1
},
getPreviousPageParam: (firstPage, allPages, firstPageParam) => {
if (firstPageParam <= 1) {
return undefined
}
return firstPageParam - 1
},
})
return useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage, allPages, lastPageParam) => {
if (lastPage.length === 0) {
return undefined
}
return lastPageParam + 1
},
getPreviousPageParam: (firstPage, allPages, firstPageParam) => {
if (firstPageParam <= 1) {
return undefined
}
return firstPageParam - 1
},
})
Um ein besseres Verständnis dafür zu bekommen, wie unendliche Abfragen unter der Haube funktionieren, lesen Sie How Infinite Queries work aus den Community-Ressourcen.