Framework
Version

Migration zu TanStack Query v5

Breaking Changes

v5 ist eine Hauptversion, daher gibt es einige Breaking Changes zu beachten

Unterstützt eine einzelne Signatur, ein Objekt

useQuery und verwandte Hooks hatten in TypeScript viele Überladungen: verschiedene Arten, wie die Funktion aufgerufen werden konnte. Das war nicht nur schwer zu warten, sondern erforderte auch eine Laufzeitprüfung, um zu sehen, welche Typen der erste und der zweite Parameter waren, um die Optionen korrekt zu erstellen.

Jetzt unterstützen wir nur noch das Objektformat.

tsx
useQuery(key, fn, options) // [!code --]
useQuery({ queryKey, queryFn, ...options }) // [!code ++]
useInfiniteQuery(key, fn, options) // [!code --]
useInfiniteQuery({ queryKey, queryFn, ...options }) // [!code ++]
useMutation(fn, options) // [!code --]
useMutation({ mutationFn, ...options }) // [!code ++]
useIsFetching(key, filters) // [!code --]
useIsFetching({ queryKey, ...filters }) // [!code ++]
useIsMutating(key, filters) // [!code --]
useIsMutating({ mutationKey, ...filters }) // [!code ++]
useQuery(key, fn, options) // [!code --]
useQuery({ queryKey, queryFn, ...options }) // [!code ++]
useInfiniteQuery(key, fn, options) // [!code --]
useInfiniteQuery({ queryKey, queryFn, ...options }) // [!code ++]
useMutation(fn, options) // [!code --]
useMutation({ mutationFn, ...options }) // [!code ++]
useIsFetching(key, filters) // [!code --]
useIsFetching({ queryKey, ...filters }) // [!code ++]
useIsMutating(key, filters) // [!code --]
useIsMutating({ mutationKey, ...filters }) // [!code ++]
tsx
queryClient.isFetching(key, filters) // [!code --]
queryClient.isFetching({ queryKey, ...filters }) // [!code ++]
queryClient.ensureQueryData(key, filters) // [!code --]
queryClient.ensureQueryData({ queryKey, ...filters }) // [!code ++]
queryClient.getQueriesData(key, filters) // [!code --]
queryClient.getQueriesData({ queryKey, ...filters }) // [!code ++]
queryClient.setQueriesData(key, updater, filters, options) // [!code --]
queryClient.setQueriesData({ queryKey, ...filters }, updater, options) // [!code ++]
queryClient.removeQueries(key, filters) // [!code --]
queryClient.removeQueries({ queryKey, ...filters }) // [!code ++]
queryClient.resetQueries(key, filters, options) // [!code --]
queryClient.resetQueries({ queryKey, ...filters }, options) // [!code ++]
queryClient.cancelQueries(key, filters, options) // [!code --]
queryClient.cancelQueries({ queryKey, ...filters }, options) // [!code ++]
queryClient.invalidateQueries(key, filters, options) // [!code --]
queryClient.invalidateQueries({ queryKey, ...filters }, options) // [!code ++]
queryClient.refetchQueries(key, filters, options) // [!code --]
queryClient.refetchQueries({ queryKey, ...filters }, options) // [!code ++]
queryClient.fetchQuery(key, fn, options) // [!code --]
queryClient.fetchQuery({ queryKey, queryFn, ...options }) // [!code ++]
queryClient.prefetchQuery(key, fn, options) // [!code --]
queryClient.prefetchQuery({ queryKey, queryFn, ...options }) // [!code ++]
queryClient.fetchInfiniteQuery(key, fn, options) // [!code --]
queryClient.fetchInfiniteQuery({ queryKey, queryFn, ...options }) // [!code ++]
queryClient.prefetchInfiniteQuery(key, fn, options) // [!code --]
queryClient.prefetchInfiniteQuery({ queryKey, queryFn, ...options }) // [!code ++]
queryClient.isFetching(key, filters) // [!code --]
queryClient.isFetching({ queryKey, ...filters }) // [!code ++]
queryClient.ensureQueryData(key, filters) // [!code --]
queryClient.ensureQueryData({ queryKey, ...filters }) // [!code ++]
queryClient.getQueriesData(key, filters) // [!code --]
queryClient.getQueriesData({ queryKey, ...filters }) // [!code ++]
queryClient.setQueriesData(key, updater, filters, options) // [!code --]
queryClient.setQueriesData({ queryKey, ...filters }, updater, options) // [!code ++]
queryClient.removeQueries(key, filters) // [!code --]
queryClient.removeQueries({ queryKey, ...filters }) // [!code ++]
queryClient.resetQueries(key, filters, options) // [!code --]
queryClient.resetQueries({ queryKey, ...filters }, options) // [!code ++]
queryClient.cancelQueries(key, filters, options) // [!code --]
queryClient.cancelQueries({ queryKey, ...filters }, options) // [!code ++]
queryClient.invalidateQueries(key, filters, options) // [!code --]
queryClient.invalidateQueries({ queryKey, ...filters }, options) // [!code ++]
queryClient.refetchQueries(key, filters, options) // [!code --]
queryClient.refetchQueries({ queryKey, ...filters }, options) // [!code ++]
queryClient.fetchQuery(key, fn, options) // [!code --]
queryClient.fetchQuery({ queryKey, queryFn, ...options }) // [!code ++]
queryClient.prefetchQuery(key, fn, options) // [!code --]
queryClient.prefetchQuery({ queryKey, queryFn, ...options }) // [!code ++]
queryClient.fetchInfiniteQuery(key, fn, options) // [!code --]
queryClient.fetchInfiniteQuery({ queryKey, queryFn, ...options }) // [!code ++]
queryClient.prefetchInfiniteQuery(key, fn, options) // [!code --]
queryClient.prefetchInfiniteQuery({ queryKey, queryFn, ...options }) // [!code ++]
tsx
queryCache.find(key, filters) // [!code --]
queryCache.find({ queryKey, ...filters }) // [!code ++]
queryCache.findAll(key, filters) // [!code --]
queryCache.findAll({ queryKey, ...filters }) // [!code ++]
queryCache.find(key, filters) // [!code --]
queryCache.find({ queryKey, ...filters }) // [!code ++]
queryCache.findAll(key, filters) // [!code --]
queryCache.findAll({ queryKey, ...filters }) // [!code ++]

queryClient.getQueryData akzeptiert jetzt nur die queryKey als Argument

Das Argument queryClient.getQueryData wurde geändert, um nur eine queryKey zu akzeptieren

tsx
queryClient.getQueryData(queryKey, filters) // [!code --]
queryClient.getQueryData(queryKey) // [!code ++]
queryClient.getQueryData(queryKey, filters) // [!code --]
queryClient.getQueryData(queryKey) // [!code ++]

queryClient.getQueryState akzeptiert jetzt nur die queryKey als Argument

Das Argument queryClient.getQueryState wurde geändert, um nur eine queryKey zu akzeptieren

tsx
queryClient.getQueryState(queryKey, filters) // [!code --]
queryClient.getQueryState(queryKey) // [!code ++]
queryClient.getQueryState(queryKey, filters) // [!code --]
queryClient.getQueryState(queryKey) // [!code ++]

Codemod

Um die Migration der remove-Überladungen zu erleichtern, bietet v5 einen Codemod.

Der Codemod ist ein Best-Efforts-Versuch, Ihnen bei der Migration von Breaking Changes zu helfen. Bitte überprüfen Sie den generierten Code gründlich! Außerdem gibt es Randfälle, die der Code-Mod nicht finden kann, also achten Sie bitte auf die Log-Ausgabe.

Wenn Sie ihn auf Dateien mit der Endung .js oder .jsx anwenden möchten, verwenden Sie den folgenden Befehl

npx jscodeshift@latest ./path/to/src/ \
  --extensions=js,jsx \
  --transform=./node_modules/@tanstack/react-query/build/codemods/src/v5/remove-overloads/remove-overloads.cjs
npx jscodeshift@latest ./path/to/src/ \
  --extensions=js,jsx \
  --transform=./node_modules/@tanstack/react-query/build/codemods/src/v5/remove-overloads/remove-overloads.cjs

Wenn Sie ihn auf Dateien mit der Endung .ts oder .tsx anwenden möchten, verwenden Sie den folgenden Befehl

npx jscodeshift@latest ./path/to/src/ \
  --extensions=ts,tsx \
  --parser=tsx \
  --transform=./node_modules/@tanstack/react-query/build/codemods/src/v5/remove-overloads/remove-overloads.cjs
npx jscodeshift@latest ./path/to/src/ \
  --extensions=ts,tsx \
  --parser=tsx \
  --transform=./node_modules/@tanstack/react-query/build/codemods/src/v5/remove-overloads/remove-overloads.cjs

Bitte beachten Sie, dass Sie im Fall von TypeScript tsx als Parser verwenden müssen, andernfalls wird der Codemod nicht richtig angewendet!

Hinweis: Die Anwendung des Codemods kann Ihre Codeformatierung beeinträchtigen. Vergessen Sie also nicht, prettier und/oder eslint auszuführen, nachdem Sie den Codemod angewendet haben!

Ein paar Hinweise, wie der Codemod funktioniert

  • Generell suchen wir nach dem glücklichen Fall, wenn der erste Parameter ein Objekt-Ausdruck ist und die Eigenschaft "queryKey" oder "mutationKey" enthält (abhängig davon, welcher Hook/welche Methode transformiert wird). Wenn dies der Fall ist, entspricht Ihr Code bereits der neuen Signatur, sodass der Codemod ihn nicht anrührt. 🎉
  • Wenn die obige Bedingung nicht erfüllt ist, prüft der Codemod, ob der erste Parameter ein Array-Ausdruck oder ein Bezeichner ist, der auf einen Array-Ausdruck verweist. Wenn dies der Fall ist, wird der Codemod ihn in einen Objekt-Ausdruck setzen, der dann der erste Parameter sein wird.
  • Wenn Objektparameter abgeleitet werden können, versucht der Codemod, die bereits vorhandenen Eigenschaften in die neu erstellte zu kopieren.
  • Wenn der Codemod die Verwendung nicht ableiten kann, hinterlässt er eine Nachricht in der Konsole. Die Nachricht enthält den Dateinamen und die Zeilennummer der Verwendung. In diesem Fall müssen Sie die Migration manuell durchführen.
  • Wenn die Transformation zu einem Fehler führt, sehen Sie ebenfalls eine Nachricht in der Konsole. Diese Nachricht informiert Sie darüber, dass etwas Unerwartetes passiert ist, und Sie müssen die Migration manuell durchführen.

Callbacks bei useQuery (und QueryObserver) wurden entfernt

onSuccess, onError und onSettled wurden aus Queries entfernt. Sie wurden für Mutationen nicht angefasst. Bitte siehe dieses RFC für die Motivation hinter dieser Änderung und was stattdessen zu tun ist.

Die Callback-Funktion refetchInterval erhält nur query übergeben

Dies optimiert die Art und Weise, wie Callbacks aufgerufen werden (die Callbacks refetchOnWindowFocus, refetchOnMount und refetchOnReconnect erhalten alle ebenfalls nur die Query übergeben), und es behebt einige Typisierungsprobleme, wenn Callbacks Daten erhalten, die von select transformiert wurden.

tsx
- refetchInterval: number | false | ((data: TData | undefined, query: Query) => number | false | undefined) // [!code --]
+ refetchInterval: number | false | ((query: Query) => number | false | undefined) // [!code ++]
- refetchInterval: number | false | ((data: TData | undefined, query: Query) => number | false | undefined) // [!code --]
+ refetchInterval: number | false | ((query: Query) => number | false | undefined) // [!code ++]

Sie können immer noch auf Daten über query.state.data zugreifen, aber es werden keine Daten sein, die von select transformiert wurden. Wenn Sie auf die transformierten Daten zugreifen müssen, können Sie die Transformation erneut auf query.state.data anwenden.

Die Methode remove wurde aus useQuery entfernt

Zuvor entfernte die remove-Methode die Query aus der queryCache, ohne Observer darüber zu informieren. Sie wurde am besten verwendet, um Daten imperativ zu entfernen, die nicht mehr benötigt wurden, z. B. beim Abmelden eines Benutzers.

Aber es macht wenig Sinn, dies zu tun, während eine Query noch aktiv ist, da dies beim nächsten Re-Rendering einfach einen harten Ladezustand auslöst.

Wenn Sie immer noch eine Query entfernen müssen, können Sie queryClient.removeQueries({queryKey: key}) verwenden

tsx
const queryClient = useQueryClient()
const query = useQuery({ queryKey, queryFn })

query.remove() // [!code --]
queryClient.removeQueries({ queryKey }) // [!code ++]
const queryClient = useQueryClient()
const query = useQuery({ queryKey, queryFn })

query.remove() // [!code --]
queryClient.removeQueries({ queryKey }) // [!code ++]

Die Mindestanforderung für TypeScript ist jetzt 4.7

Hauptsächlich, weil ein wichtiger Fix bezüglich der Typinferenz veröffentlicht wurde. Weitere Informationen finden Sie in diesem TypeScript-Problem.

Die Option isDataEqual wurde aus useQuery entfernt

Zuvor wurde diese Funktion verwendet, um anzugeben, ob die vorherigen Daten (true) oder die neuen Daten (false) als aufgelöste Daten für die Query verwendet werden sollten.

Sie können die gleiche Funktionalität erreichen, indem Sie stattdessen eine Funktion an structuralSharing übergeben

tsx
import { replaceEqualDeep } from '@tanstack/react-query'

- isDataEqual: (oldData, newData) => customCheck(oldData, newData) // [!code --]
+ structuralSharing: (oldData, newData) => customCheck(oldData, newData) ? oldData : replaceEqualDeep(oldData, newData) // [!code ++]
import { replaceEqualDeep } from '@tanstack/react-query'

- isDataEqual: (oldData, newData) => customCheck(oldData, newData) // [!code --]
+ structuralSharing: (oldData, newData) => customCheck(oldData, newData) ? oldData : replaceEqualDeep(oldData, newData) // [!code ++]

Der veraltete benutzerdefinierte Logger wurde entfernt

Benutzerdefinierte Logger waren bereits in v4 veraltet und wurden in dieser Version entfernt. Das Logging hatte nur Auswirkungen im Entwicklungsmodus, wo die Übergabe eines benutzerdefinierten Loggers nicht notwendig ist.

Unterstützte Browser

Wir haben unsere Browserlist aktualisiert, um ein moderneres, performanteres und kleineres Bundle zu erzeugen. Die Anforderungen können Sie hier nachlesen.

Private Klassenfelder und -methoden

TanStack Query hatte schon immer private Felder und Methoden in Klassen, aber sie waren nicht wirklich privat – sie waren nur in TypeScript privat. Wir verwenden jetzt ECMAScript Private Class Fields, was bedeutet, dass diese Felder jetzt wirklich privat sind und zur Laufzeit nicht von außen zugegriffen werden können.

Benenne cacheTime in gcTime um

Fast jeder versteht cacheTime falsch. Es klingt wie "die Zeit, in der Daten gecacht werden", aber das ist nicht korrekt.

cacheTime tut nichts, solange eine Query noch in Gebrauch ist. Sie greift erst ein, sobald die Query nicht mehr genutzt wird. Nach Ablauf der Zeit werden Daten "garbage collected", um zu verhindern, dass der Cache wächst.

gc bezieht sich auf die "Garbage Collect"-Zeit. Es ist etwas technischer, aber auch eine ziemlich bekannte Abkürzung in der Informatik.

tsx
const MINUTE = 1000 * 60;

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
-      cacheTime: 10 * MINUTE, // [!code --]
+      gcTime: 10 * MINUTE, // [!code ++]
    },
  },
})
const MINUTE = 1000 * 60;

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
-      cacheTime: 10 * MINUTE, // [!code --]
+      gcTime: 10 * MINUTE, // [!code ++]
    },
  },
})

Die Option useErrorBoundary wurde in throwOnError umbenannt

Um die Option useErrorBoundary Framework-unabhängiger zu gestalten und Verwechslungen mit dem etablierten React-Funktionspräfix "use" für Hooks und dem Komponentennamen "ErrorBoundary" zu vermeiden, wurde sie in throwOnError umbenannt, um ihre Funktionalität genauer widerzuspiegeln.

TypeScript: Error ist jetzt der Standardtyp für Fehler anstelle von unknown

Obwohl man in JavaScript alles throwen kann (was unknown zum korrektesten Typ macht), werden fast immer Errors (oder Unterklassen von Error) geworfen. Diese Änderung erleichtert in den meisten Fällen die Arbeit mit dem error-Feld in TypeScript.

Wenn Sie etwas werfen möchten, das kein Error ist, müssen Sie jetzt den generischen Typ selbst festlegen

ts
useQuery<number, string>({
  queryKey: ['some-query'],
  queryFn: async () => {
    if (Math.random() > 0.5) {
      throw 'some error'
    }
    return 42
  },
})
useQuery<number, string>({
  queryKey: ['some-query'],
  queryFn: async () => {
    if (Math.random() > 0.5) {
      throw 'some error'
    }
    return 42
  },
})

Für eine Möglichkeit, einen anderen Fehlertyp global festzulegen, siehe den TypeScript-Leitfaden.

Die eslint-Regel prefer-query-object-syntax wurde entfernt

Da nun nur noch die Objektsyntax unterstützt wird, ist diese Regel nicht mehr erforderlich

keepPreviousData wurde zugunsten der Identitätsfunktion placeholderData entfernt

Wir haben die Option keepPreviousData und das Flag isPreviousData entfernt, da sie größtenteils das Gleiche taten wie placeholderData und das Flag isPlaceholderData.

Um die gleiche Funktionalität wie keepPreviousData zu erreichen, haben wir die vorherigen Query-Daten als Argument für placeholderData hinzugefügt, das eine Identitätsfunktion akzeptiert. Daher müssen Sie nur eine Identitätsfunktion für placeholderData angeben oder die enthaltene Funktion keepPreviousData von Tanstack Query verwenden.

Ein Hinweis hierbei ist, dass useQueries previousData in der placeholderData-Funktion nicht als Argument erhält. Dies liegt an der dynamischen Natur der im Array übergebenen Queries, was zu einer anderen Form des Ergebnisses als bei placeholder und queryFn führen kann.

tsx
import {
   useQuery,
+  keepPreviousData // [!code ++]
} from "@tanstack/react-query";

const {
   data,
-  isPreviousData, // [!code --]
+  isPlaceholderData, // [!code ++]
} = useQuery({
  queryKey,
  queryFn,
- keepPreviousData: true, // [!code --]
+ placeholderData: keepPreviousData // [!code ++]
});
import {
   useQuery,
+  keepPreviousData // [!code ++]
} from "@tanstack/react-query";

const {
   data,
-  isPreviousData, // [!code --]
+  isPlaceholderData, // [!code ++]
} = useQuery({
  queryKey,
  queryFn,
- keepPreviousData: true, // [!code --]
+ placeholderData: keepPreviousData // [!code ++]
});

Eine Identitätsfunktion bezieht sich im Kontext von Tanstack Query auf eine Funktion, die immer ihr übergebenes Argument (d.h. Daten) unverändert zurückgibt.

ts
useQuery({
  queryKey,
  queryFn,
  placeholderData: (previousData, previousQuery) => previousData, // identity function with the same behaviour as `keepPreviousData`
})
useQuery({
  queryKey,
  queryFn,
  placeholderData: (previousData, previousQuery) => previousData, // identity function with the same behaviour as `keepPreviousData`
})

Es gibt jedoch einige Vorbehalte bei dieser Änderung, die Sie beachten müssen

  • placeholderData versetzt Sie immer in den success-Status, während keepPreviousData Ihnen den Status der vorherigen Query gab. Dieser Status könnte error sein, wenn wir Daten erfolgreich abgerufen und dann einen Hintergrund-Refetch-Fehler erhalten haben. Der Fehler selbst wurde jedoch nicht geteilt, daher entschieden wir uns, beim Verhalten von placeholderData zu bleiben.

  • keepPreviousData gab Ihnen den dataUpdatedAt-Zeitstempel der vorherigen Daten, während mit placeholderData dataUpdatedAt bei 0 bleibt. Dies kann ärgerlich sein, wenn Sie diesen Zeitstempel kontinuierlich auf dem Bildschirm anzeigen möchten. Sie können dies jedoch mit useEffect umgehen.

    ts
    const [updatedAt, setUpdatedAt] = useState(0)
    
    const { data, dataUpdatedAt } = useQuery({
      queryKey: ['projects', page],
      queryFn: () => fetchProjects(page),
    })
    
    useEffect(() => {
      if (dataUpdatedAt > updatedAt) {
        setUpdatedAt(dataUpdatedAt)
      }
    }, [dataUpdatedAt])
    
    const [updatedAt, setUpdatedAt] = useState(0)
    
    const { data, dataUpdatedAt } = useQuery({
      queryKey: ['projects', page],
      queryFn: () => fetchProjects(page),
    })
    
    useEffect(() => {
      if (dataUpdatedAt > updatedAt) {
        setUpdatedAt(dataUpdatedAt)
      }
    }, [dataUpdatedAt])
    

Das Refetching bei Fensterfokus lauscht nicht mehr auf das focus-Ereignis

Das visibilitychange-Ereignis wird jetzt ausschließlich verwendet. Dies ist möglich, da wir nur Browser unterstützen, die das visibilitychange-Ereignis unterstützen. Dies behebt eine Reihe von Problemen, wie hier aufgelistet.

Der Netzwerkstatus stützt sich nicht mehr auf die Eigenschaft navigator.onLine

navigator.onLine funktioniert in Chromium-basierten Browsern nicht gut. Es gibt viele Probleme mit negativen Fehlalarmen, die dazu führen, dass Queries fälschlicherweise als offline markiert werden.

Um dies zu umgehen, beginnen wir nun immer mit online: true und lauschen nur auf online und offline Ereignisse, um den Status zu aktualisieren.

Dies sollte die Wahrscheinlichkeit von falschen Negativen verringern, kann aber zu falschen Positiven für Offline-Anwendungen führen, die über ServiceWorker geladen werden und auch ohne Internetverbindung funktionieren können.

Die benutzerdefinierte context-Prop wurde zugunsten einer benutzerdefinierten queryClient-Instanz entfernt

In v4 haben wir die Möglichkeit eingeführt, benutzerdefinierte context an alle react-query Hooks zu übergeben. Dies ermöglichte eine ordnungsgemäße Isolierung bei der Verwendung von MicroFrontends.

context ist jedoch nur ein React-Feature. Alles, was context tut, ist uns den Zugriff auf den queryClient zu ermöglichen. Wir könnten die gleiche Isolierung erreichen, indem wir zulassen, dass ein benutzerdefinierter queryClient direkt übergeben wird. Dies wird es anderen Frameworks ermöglichen, die gleiche Funktionalität auf Framework-unabhängige Weise zu haben.

tsx
import { queryClient } from './my-client'

const { data } = useQuery(
  {
    queryKey: ['users', id],
    queryFn: () => fetch(...),
-   context: customContext // [!code --]
  },
+  queryClient, // [!code ++]
)
import { queryClient } from './my-client'

const { data } = useQuery(
  {
    queryKey: ['users', id],
    queryFn: () => fetch(...),
-   context: customContext // [!code --]
  },
+  queryClient, // [!code ++]
)

refetchPage wurde zugunsten von maxPages entfernt

In v4 haben wir die Möglichkeit eingeführt, die Seiten für Infinite Queries mit der Funktion refetchPage zu definieren.

Das erneute Abrufen aller Seiten kann jedoch zu UI-Inkonsistenzen führen. Außerdem ist diese Option z. B. bei queryClient.refetchQueries verfügbar, aber sie bewirkt nur etwas für Infinite Queries, nicht für "normale" Queries.

v5 enthält eine neue Option maxPages für Infinite Queries, um die Anzahl der Seiten zu begrenzen, die in den Query-Daten gespeichert und erneut abgerufen werden. Dieses neue Feature deckt die Anwendungsfälle ab, die ursprünglich für die refetchPage-Seitenfunktion identifiziert wurden, ohne die damit verbundenen Probleme.

Neue dehydrate API

Die Optionen, die Sie an dehydrate übergeben können, wurden vereinfacht. Queries und Mutationen werden immer dehydriert (gemäß der Standardfunktionsimplementierung). Um dieses Verhalten zu ändern, können Sie anstelle der entfernten booleschen Optionen dehydrateMutations und dehydrateQueries die Funktionsäquivalente shouldDehydrateQuery oder shouldDehydrateMutation implementieren. Um das alte Verhalten, Queries/Mutationen überhaupt nicht zu hydrieren, zu erhalten, übergeben Sie () => false.

tsx
- dehydrateMutations?: boolean // [!code --]
- dehydrateQueries?: boolean // [!code --]
- dehydrateMutations?: boolean // [!code --]
- dehydrateQueries?: boolean // [!code --]

Infinite Queries benötigen jetzt eine initialPageParam

Zuvor haben wir undefined an die queryFn als pageParam übergeben, und Sie konnten einen Standardwert für den Parameter pageParam in der queryFn-Funktionssignatur zuweisen. Dies hatte den Nachteil, dass undefined in der queryCache gespeichert wurde, was nicht serialisierbar ist.

Stattdessen müssen Sie jetzt eine explizite initialPageParam an die Infinite Query-Optionen übergeben. Diese wird als pageParam für die erste Seite verwendet

tsx
useInfiniteQuery({
   queryKey,
-  queryFn: ({ pageParam = 0 }) => fetchSomething(pageParam), // [!code --]
+  queryFn: ({ pageParam }) => fetchSomething(pageParam), // [!code ++]
+  initialPageParam: 0, // [!code ++]
   getNextPageParam: (lastPage) => lastPage.next,
})
useInfiniteQuery({
   queryKey,
-  queryFn: ({ pageParam = 0 }) => fetchSomething(pageParam), // [!code --]
+  queryFn: ({ pageParam }) => fetchSomething(pageParam), // [!code ++]
+  initialPageParam: 0, // [!code ++]
   getNextPageParam: (lastPage) => lastPage.next,
})

Der manuelle Modus für Infinite Queries wurde entfernt

Zuvor erlaubten wir, die pageParams, die von getNextPageParam oder getPreviousPageParam zurückgegeben wurden, zu überschreiben, indem ein pageParam-Wert direkt an fetchNextPage oder fetchPreviousPage übergeben wurde. Dieses Feature funktionierte bei Refetches überhaupt nicht und war nicht allgemein bekannt oder genutzt. Das bedeutet auch, dass getNextPageParam für Infinite Queries jetzt erforderlich ist.

Die Rückgabe von null von getNextPageParam oder getPreviousPageParam zeigt jetzt an, dass keine weitere Seite verfügbar ist

In v4 mussten Sie explizit undefined zurückgeben, um anzuzeigen, dass keine weitere Seite verfügbar ist. Wir haben diese Prüfung erweitert, um auch null einzuschließen.

Keine Wiederholungsversuche auf dem Server

Auf dem Server hat retry jetzt standardmäßig 0 statt 3. Für Prefetching haben wir schon immer standardmäßig 0 Wiederholungsversuche verwendet, aber da Queries mit suspense jetzt auch auf dem Server ausgeführt werden können (seit React18), müssen wir sicherstellen, dass wir auf dem Server überhaupt keine Wiederholungsversuche durchführen.

status: loading wurde in status: pending geändert, und isLoading wurde in isPending geändert, und isInitialLoading wurde jetzt in isLoading umbenannt

Der loading-Status wurde in pending umbenannt, und ebenso wurde das abgeleitete Flag isLoading in isPending umbenannt.

Auch für Mutationen wurde der status von loading in pending geändert und das isLoading-Flag wurde in isPending geändert.

Zuletzt wurde ein neues abgeleitetes Flag isLoading zu den Queries hinzugefügt, das als isPending && isFetching implementiert ist. Das bedeutet, dass isLoading und isInitialLoading dasselbe bedeuten, aber isInitialLoading nun veraltet ist und in der nächsten Hauptversion entfernt wird.

Um die Gründe für diese Änderung zu verstehen, schauen Sie sich die v5 Roadmap-Diskussion an.

hashQueryKey wurde in hashKey umbenannt

da es auch Mutationsschlüssel hasht und innerhalb der predicate-Funktionen von useIsMutating und useMutationState verwendet werden kann, denen Mutationen übergeben werden.

Die Mindestanforderung für React ist jetzt 18.0

React Query v5 erfordert React 18.0 oder höher. Dies liegt daran, dass wir den neuen useSyncExternalStore Hook verwenden, der nur in React 18.0 und höher verfügbar ist. Zuvor haben wir das von React bereitgestellte Shim verwendet.

Die Prop contextSharing wurde von QueryClientProvider entfernt

Sie konnten zuvor die Eigenschaft contextSharing verwenden, um die erste (und mindestens eine) Instanz des Query-Client-Kontextes über das Fenster hinweg zu teilen. Dies stellte sicher, dass, wenn TanStack Query über verschiedene Bundles oder Microfrontends hinweg verwendet wurde, sie alle die gleiche Instanz des Kontextes verwendeten, unabhängig von der Modul-Scope.

Mit der Entfernung der benutzerdefinierten Kontext-Prop in v5, siehe den Abschnitt über Entfernte benutzerdefinierte Kontext-Prop zugunsten einer benutzerdefinierten queryClient-Instanz. Wenn Sie denselben Query-Client über mehrere Pakete einer Anwendung hinweg teilen möchten, können Sie direkt eine geteilte benutzerdefinierte queryClient-Instanz übergeben.

unstable_batchedUpdates wird nicht mehr als Batch-Funktion in React und React Native verwendet

Da die Funktion unstable_batchedUpdates in React 18 ein Noop ist, wird sie nicht mehr automatisch als Batch-Funktion in react-query gesetzt.

Wenn Ihr Framework eine benutzerdefinierte Batch-Funktion unterstützt, können Sie TanStack Query dies mitteilen, indem Sie notifyManager.setBatchNotifyFunction aufrufen.

Zum Beispiel wird die Batch-Funktion in solid-query so gesetzt

ts
import { notifyManager } from '@tanstack/query-core'
import { batch } from 'solid-js'

notifyManager.setBatchNotifyFunction(batch)
import { notifyManager } from '@tanstack/query-core'
import { batch } from 'solid-js'

notifyManager.setBatchNotifyFunction(batch)

Änderungen an der Hydration-API

Um Nebenläufigkeitsfeatures und Übergänge besser zu unterstützen, haben wir einige Änderungen an den Hydrations-APIs vorgenommen. Die Komponente Hydrate wurde in HydrationBoundary umbenannt und der Hook useHydrate wurde entfernt.

Die HydrationBoundary hydriert keine Mutationen mehr, nur noch Queries. Um Mutationen zu hydrieren, verwenden Sie die Low-Level hydrate API oder das Plugin persistQueryClient.

Schließlich haben sich als technische Details die Zeitpunkte, zu denen Queries hydriert werden, leicht geändert. Neue Queries werden weiterhin in der Render-Phase hydriert, damit SSR wie gewohnt funktioniert. Jede Query, die sich bereits im Cache befindet, wird jetzt jedoch in einem Effekt hydriert (solange ihre Daten frischer sind als die im Cache befindlichen). Wenn Sie nur einmal am Anfang Ihrer Anwendung hydrieren, wie es üblich ist, wird sich dies nicht auf Sie auswirken. Wenn Sie jedoch Server Components verwenden und beim Navigieren zu einer Seite frische Daten zur Hydrierung übergeben, sehen Sie möglicherweise einen kurzen Moment alte Daten, bevor die Seite sofort neu gerendert wird.

Diese letzte Änderung ist technisch gesehen eine Breaking Change und wurde vorgenommen, damit wir Inhalte auf der *bestehenden* Seite nicht vorzeitig aktualisieren, bevor ein Seitenübergang vollständig abgeschlossen ist. Es sind keine Maßnahmen Ihrerseits erforderlich.

tsx
- import { Hydrate } from '@tanstack/react-query' // [!code --]
+ import { HydrationBoundary } from '@tanstack/react-query' // [!code ++]


- <Hydrate state={dehydratedState}> // [!code --]
+ <HydrationBoundary state={dehydratedState}> // [!code ++]
  <App />
- </Hydrate> // [!code --]
+ </HydrationBoundary> // [!code ++]
- import { Hydrate } from '@tanstack/react-query' // [!code --]
+ import { HydrationBoundary } from '@tanstack/react-query' // [!code ++]


- <Hydrate state={dehydratedState}> // [!code --]
+ <HydrationBoundary state={dehydratedState}> // [!code ++]
  <App />
- </Hydrate> // [!code --]
+ </HydrationBoundary> // [!code ++]

Änderungen an Query-Standardwerten

queryClient.getQueryDefaults wird jetzt alle übereinstimmenden Registrierungen zusammenführen, anstatt nur die erste übereinstimmende Registrierung zurückzugeben.

Infolgedessen sollten Aufrufe von queryClient.setQueryDefaults jetzt mit *zunehmender* Spezifität geordnet werden. Das heißt, Registrierungen sollten vom **allgemeinsten Schlüssel** zum **am wenigsten generischen** erfolgen.

Zum Beispiel

ts
+ queryClient.setQueryDefaults(['todo'], {   // [!code ++]
+   retry: false,  // [!code ++]
+   staleTime: 60_000,  // [!code ++]
+ })  // [!code ++]
queryClient.setQueryDefaults(['todo', 'detail'], {
+   retry: true,  // [!code --]
  retryDelay: 1_000,
  staleTime: 10_000,
})
- queryClient.setQueryDefaults(['todo'], { // [!code --]
-   retry: false, // [!code --]
-   staleTime: 60_000, // [!code --]
- }) // [!code --]
+ queryClient.setQueryDefaults(['todo'], {   // [!code ++]
+   retry: false,  // [!code ++]
+   staleTime: 60_000,  // [!code ++]
+ })  // [!code ++]
queryClient.setQueryDefaults(['todo', 'detail'], {
+   retry: true,  // [!code --]
  retryDelay: 1_000,
  staleTime: 10_000,
})
- queryClient.setQueryDefaults(['todo'], { // [!code --]
-   retry: false, // [!code --]
-   staleTime: 60_000, // [!code --]
- }) // [!code --]

Beachten Sie, dass in diesem spezifischen Beispiel retry: true zur Registrierung von ['todo', 'detail'] hinzugefügt wurde, um zu verhindern, dass es nun retry: false von der allgemeineren Registrierung erbt. Die spezifischen Änderungen, die zur Beibehaltung des genauen Verhaltens erforderlich sind, variieren je nach Ihren Standardwerten.

Neue Features 🚀

v5 kommt auch mit neuen Funktionen

Optimistische Updates vereinfacht

Wir haben eine neue, vereinfachte Möglichkeit, optimistische Updates durchzuführen, indem wir die zurückgegebenen variables von useMutation nutzen

tsx
const queryInfo = useTodos()
const addTodoMutation = useMutation({
  mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),
  onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
})

if (queryInfo.data) {
  return (
    <ul>
      {queryInfo.data.items.map((todo) => (
        <li key={todo.id}>{todo.text}</li>
      ))}
      {addTodoMutation.isPending && (
        <li key={String(addTodoMutation.submittedAt)} style={{ opacity: 0.5 }}>
          {addTodoMutation.variables}
        </li>
      )}
    </ul>
  )
}
const queryInfo = useTodos()
const addTodoMutation = useMutation({
  mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),
  onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
})

if (queryInfo.data) {
  return (
    <ul>
      {queryInfo.data.items.map((todo) => (
        <li key={todo.id}>{todo.text}</li>
      ))}
      {addTodoMutation.isPending && (
        <li key={String(addTodoMutation.submittedAt)} style={{ opacity: 0.5 }}>
          {addTodoMutation.variables}
        </li>
      )}
    </ul>
  )
}

Hier ändern wir nur, wie die Benutzeroberfläche aussieht, wenn die Mutation läuft, anstatt Daten direkt in den Cache zu schreiben. Dies funktioniert am besten, wenn wir nur einen Ort haben, an dem wir das optimistische Update anzeigen müssen. Weitere Details finden Sie in der Dokumentation zu optimistischen Updates.

Limitierte, Infinite Queries mit neuer maxPages-Option

Unendliche Abfragen sind großartig, wenn unendliches Scrollen oder Paginierung benötigt werden. Je mehr Seiten Sie jedoch abrufen, desto mehr Speicher verbrauchen Sie, und dies verlangsamt auch den Prozess des erneuten Abrufens von Abfragen, da alle Seiten sequenziell erneut abgerufen werden.

Version 5 hat eine neue Option maxPages für unendliche Abfragen, mit der Entwickler die Anzahl der Seiten begrenzen können, die in den Abfragedaten gespeichert und anschließend erneut abgerufen werden. Sie können den Wert maxPages an die UX und die Leistung beim erneuten Abrufen anpassen, die Sie liefern möchten.

Beachten Sie, dass die unendliche Liste bidirektional sein muss, was erfordert, dass sowohl getNextPageParam als auch getPreviousPageParam definiert sind.

Infinite Queries können mehrere Seiten vorab abrufen

Unendliche Abfragen können wie normale Abfragen vorab abgerufen werden. Standardmäßig wird nur die erste Seite der Abfrage vorab abgerufen und unter dem angegebenen Abfrageschlüssel gespeichert. Wenn Sie mehr als eine Seite vorab abrufen möchten, können Sie die Option pages verwenden. Lesen Sie das Handbuch zum Vorabrufen für weitere Informationen.

Neue Option combine für useQueries

Weitere Details finden Sie in der Dokumentation zu useQueries.

Experimenteller fine grained storage persister

Weitere Details finden Sie in der Dokumentation zu experimental_createPersister.

Typsichere Art, Query-Optionen zu erstellen

Weitere Details finden Sie in der TypeScript-Dokumentation.

neue Hooks für Suspense

Mit v5 wird Suspense für das Abrufen von Daten endlich "stabil". Wir haben dedizierte Hooks hinzugefügt: useSuspenseQuery, useSuspenseInfiniteQuery und useSuspenseQueries. Mit diesen Hooks ist data auf Typenebene nie mehr potenziell undefined

js
const { data: post } = useSuspenseQuery({
  // ^? const post: Post
  queryKey: ['post', postId],
  queryFn: () => fetchPost(postId),
})
const { data: post } = useSuspenseQuery({
  // ^? const post: Post
  queryKey: ['post', postId],
  queryFn: () => fetchPost(postId),
})

Das experimentelle Flag suspense: boolean bei den Abfrage-Hooks wurde entfernt.

Mehr dazu erfahren Sie in der Suspense-Dokumentation.