Framework
Version

Unendliche Queries

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

  • data ist jetzt ein Objekt, das unendliche Abfragedaten enthält
  • Ein Array data.pages, das die abgerufenen Seiten enthält
  • Ein Array data.pageParams, das die Seitenparameter enthält, die zum Abrufen der Seiten verwendet wurden
  • Die Funktionen fetchNextPage und fetchPreviousPage sind jetzt verfügbar (fetchNextPage ist erforderlich)
  • Die Option initialPageParam ist jetzt verfügbar (und erforderlich), um den anfänglichen Seitenparameter anzugeben
  • Die Optionen getNextPageParam und getPreviousPageParam sind verfügbar, um sowohl zu bestimmen, ob weitere Daten geladen werden müssen, als auch die Informationen dafür abzurufen. Diese Informationen werden als zusätzlicher Parameter in der Abfragefunktion bereitgestellt
  • Ein boolescher Wert hasNextPage ist jetzt verfügbar und ist true, wenn getNextPageParam einen Wert ungleich null oder undefined zurückgibt
  • Ein boolescher Wert hasPreviousPage ist jetzt verfügbar und ist true, wenn getPreviousPageParam einen Wert ungleich null oder undefined zurückgibt
  • Die booleschen Werte isFetchingNextPage und isFetchingPreviousPage sind jetzt verfügbar, um zwischen einem Hintergrund-Aktualisierungszustand und einem Zustand des Ladens weiterer Daten zu unterscheiden

Hinweis: Die Optionen initialData oder placeholderData müssen dieselbe Struktur aufweisen wie ein Objekt mit den Eigenschaften data.pages und data.pageParams.

Beispiel

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

tsx
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

  • Warten, bis useInfiniteQuery standardmäßig die erste Datengruppe anfordert
  • Die Informationen für die nächste Abfrage in getNextPageParam zurückgeben
  • Die Funktion fetchNextPage aufrufen
tsx
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.

jsx
<List onEndReached={() => hasNextPage && !isFetching && fetchNextPage()} />
<List onEndReached={() => hasNextPage && !isFetching && fetchNextPage()} />

Was passiert, wenn eine unendliche Abfrage erneut abgerufen werden muss?

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.

Was, wenn ich eine bidirektionale unendliche Liste implementieren möchte?

Bidirektionale Listen können durch die Verwendung der Eigenschaften und Funktionen getPreviousPageParam, fetchPreviousPage, hasPreviousPage und isFetchingPreviousPage implementiert werden.

tsx
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,
})

Was, wenn ich die Seiten in umgekehrter Reihenfolge anzeigen möchte?

Manchmal möchten Sie die Seiten in umgekehrter Reihenfolge anzeigen. In diesem Fall können Sie die Option select verwenden

tsx
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(),
  }),
})

Was, wenn ich die unendliche Abfrage manuell aktualisieren möchte?

Manuelles Entfernen der ersten Seite:

tsx
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),
}))

Manuelles Entfernen eines einzelnen Werts aus einer einzelnen Seite

tsx
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,
}))

Nur die erste Seite behalten

tsx
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!

Was, wenn ich die Anzahl der Seiten begrenzen möchte?

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

  • wenn der Benutzer eine große Anzahl von Seiten laden kann (Speichernutzung)
  • wenn Sie eine unendliche Abfrage erneut abrufen müssen, die Dutzende von Seiten enthält (Netzwerknutzung: alle Seiten werden sequenziell abgerufen)

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.

tsx
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,
})

Was, wenn meine API keinen Cursor zurückgibt?

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.

tsx
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
  },
})

Weiterführende Lektüre

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.