TanStack Query in React Native: React Query 5 Guide (2026)

TanStack Query 5 in React Native: QueryClient für Expo, focusManager, onlineManager, Offline-Persistenz mit AsyncStorage, optimistische Mutations und ein klarer Vergleich zu Zustand. Mit Code-Beispielen für die New Architecture.

TanStack Query 5 in React Native (2026)

Aktualisiert: 29. Juni 2026

TanStack Query in React Native ist eine Library für asynchrones Server-State-Management, die HTTP-Antworten zwischenspeichert, automatisch im Hintergrund aktualisiert und Offline-Verhalten orchestriert – inklusive App-Lifecycle- und Netzwerk-Events, die im Mobile-Kontext entscheidend sind. Mit React Query 5 unterstützt die Bibliothek seit 2024 die New Architecture, Suspense-Integration und persistierten Cache aus dem Stand. Aus meiner Erfahrung als Web-zu-Mobile-Entwicklerin ist sie der schnellste Weg, REST- oder GraphQL-Endpunkte stabil ans UI zu binden, ohne ein eigenes Caching-Layer zu bauen.

  • TanStack Query 5 (Stand Juni 2026: v5.62) ist vollständig kompatibel mit React Native 0.81, der New Architecture und Expo SDK 55.
  • Die Mobile-Integration erfordert focusManager und onlineManager, weil Browser-APIs wie window.focus und navigator.onLine in React Native fehlen.
  • Mit @tanstack/query-async-storage-persister und persistQueryClient überlebt der Cache App-Restarts und liefert Offline-First-Verhalten.
  • Optimistische Updates über onMutate machen Listen-Interaktionen flüssig, auch bei 4G-Latenzen von 200–400 ms.
  • TanStack Query ersetzt nicht Zustand oder Redux – es deckt nur den Server-State ab und ergänzt globale Client-Stores sauber.
  • Mit React Native DevTools (Hermes) lassen sich Query-States seit 0.79 direkt im Component Tree inspizieren.

Was ist TanStack Query und warum in React Native?

TanStack Query – vor v4 als React Query bekannt – ist eine deklarative Server-State-Library, die das Konzept von Cache, Stale-Time und Background-Refetch in einen Hook verpackt. Statt manuell useEffect, Loading-Flags und Abbruch-Logik zu schreiben, abonnierst du Daten über useQuery und beschreibst nur, wie sie geladen werden. Die Library kümmert sich um Deduplizierung paralleler Requests, Retry-Strategien, Refetching nach Fokus-Wechseln und das Zusammenführen mehrerer Komponenten, die denselben Endpunkt brauchen.

In React Native ist das besonders wertvoll. Mobile Apps wechseln häufig zwischen Foreground und Background, verlieren das Netz im Tunnel und müssen mit Speicherlimits umgehen. Eine handgeschriebene Lösung für all das ist erfahrungsgemäß die zweitgrößte Quelle für Bugs nach Navigation. TanStack Query 5 löst diese Klasse von Problemen mit einer API, die – und das ist der Punkt für alle, die wie ich aus React Web kommen – fast identisch zur Web-Version ist. Was du auf tanstack.com/query liest, gilt zu 95 % auch hier; die Unterschiede betreffen Lifecycle-Adapter, nicht die Hook-Signaturen.

Installation und QueryClient-Setup für Expo

Für Expo SDK 55 und React Native 0.81 reichen drei Pakete: die Library selbst, der AsyncStorage-Persister und – optional, aber empfohlen – @react-native-community/netinfo für den Online-Manager. Mit Expo:

npx expo install @tanstack/react-query \
  @tanstack/query-async-storage-persister \
  @tanstack/react-query-persist-client \
  @react-native-async-storage/async-storage \
  @react-native-community/netinfo

Den QueryClient initialisierst du genau einmal pro App und stellst ihn per Provider bereit. Wichtig: Die Default-Optionen müssen für Mobile angepasst werden – ein staleTime von 0 (Web-Default) verursacht jedes Mal beim Re-Mount einer Screen-Komponente einen Refetch, was in React Navigation bei jedem Stack-Pop passiert.

// app/_layout.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useState } from 'react';
import { Slot } from 'expo-router';

export default function RootLayout() {
  const [client] = useState(() => new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 1000 * 60,      // 1 Minute frisch
        gcTime: 1000 * 60 * 60 * 24, // 24h im Cache
        retry: 2,
        refetchOnReconnect: 'always',
      },
    },
  }));

  return (
    <QueryClientProvider client={client}>
      <Slot />
    </QueryClientProvider>
  );
}

Der useState-Initializer ist kein Stilmittel, sondern verhindert, dass der Client bei jedem Re-Render neu entsteht – ein Anti-Pattern, das ich in vielen Migrations-PRs gesehen habe.

focusManager und onlineManager konfigurieren

Hier kommt der erste echte Web-zu-Mobile-Bruch. Im Browser nutzt TanStack Query window.addEventListener('focus', …), um Daten zu refetchen, sobald der User auf den Tab zurückkehrt. In React Native existiert dieses Event nicht – stattdessen gibt es AppState. Genauso fehlt navigator.onLine; das Pendant ist NetInfo.

Beide Adapter musst du manuell verdrahten. Ich lege das in eine separate Datei, die ich einmal in der App-Root importiere:

// lib/queryAdapters.ts
import { AppState, Platform } from 'react-native';
import NetInfo from '@react-native-community/netinfo';
import { focusManager, onlineManager } from '@tanstack/react-query';

onlineManager.setEventListener(setOnline => {
  return NetInfo.addEventListener(state => {
    setOnline(!!state.isConnected);
  });
});

export function useAppFocus() {
  AppState.addEventListener('change', status => {
    if (Platform.OS !== 'web') {
      focusManager.setFocused(status === 'active');
    }
  });
}

useQuery in der Praxis: Daten fetchen

Ein typischer Screen, der eine Artikel-Liste lädt, sieht so aus. Achte auf den queryKey – er ist der Cache-Schlüssel und sollte alle Parameter enthalten, die das Ergebnis beeinflussen:

// app/articles.tsx
import { useQuery } from '@tanstack/react-query';
import { FlatList, Text, View, ActivityIndicator } from 'react-native';

type Article = { id: string; title: string };

async function fetchArticles(category: string): Promise<Article[]> {
  const res = await fetch(`https://api.example.com/articles?cat=${category}`);
  if (!res.ok) throw new Error('Fetch fehlgeschlagen');
  return res.json();
}

export default function ArticlesScreen() {
  const category = 'react-native';
  const { data, isPending, error, refetch, isRefetching } = useQuery({
    queryKey: ['articles', category],
    queryFn: () => fetchArticles(category),
  });

  if (isPending) return <ActivityIndicator />;
  if (error) return <Text>Fehler: {error.message}</Text>;

  return (
    <FlatList
      data={data}
      keyExtractor={item => item.id}
      onRefresh={refetch}
      refreshing={isRefetching}
      renderItem={({ item }) => <Text>{item.title}</Text>}
    />
  );
}

Beachte zwei Details: isPending ersetzt das alte isLoading aus v4 – ein Breaking Change, der in den v5 Migration Notes dokumentiert ist. Und refetch lässt sich direkt an FlatList.onRefresh binden – Pull-to-Refresh ohne eine einzige Zeile State-Code.

Mutations und optimistische Updates

Mutations decken Schreibvorgänge ab – POST, PUT, DELETE. Die echte Stärke kommt mit onMutate, das eine optimistische UI-Aktualisierung erlaubt, bevor der Server antwortet. Bei einer Like-Funktion in einer Social-App mit 300 ms Latenz fühlt sich das den Unterschied zwischen "träge" und "nativ" an:

import { useMutation, useQueryClient } from '@tanstack/react-query';

function useLikeArticle() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: (id: string) =>
      fetch(`/api/articles/${id}/like`, { method: 'POST' }),
    onMutate: async (id) => {
      await qc.cancelQueries({ queryKey: ['articles'] });
      const prev = qc.getQueryData<Article[]>(['articles']);
      qc.setQueryData<Article[]>(['articles'], old =>
        old?.map(a => a.id === id ? { ...a, liked: true } : a)
      );
      return { prev };
    },
    onError: (_err, _id, ctx) => {
      if (ctx?.prev) qc.setQueryData(['articles'], ctx.prev);
    },
    onSettled: () => qc.invalidateQueries({ queryKey: ['articles'] }),
  });
}

Das Pattern ist immer dasselbe: Vor dem Request den vorherigen Stand sichern, optimistisch ändern, bei Fehler zurückrollen, am Ende invalidieren. Wenn das in deiner App häufig vorkommt, lohnt sich eine eigene useOptimisticUpdate-Abstraktion – aber erst, wenn du das Pattern 3+ Mal geschrieben hast. Vorher ist es vorzeitige Abstraktion.

Offline-Persistenz mit AsyncStorage

Ein leerer Cache nach App-Restart ist auf Mobile ein UX-Bruch, den Web-Nutzer nicht kennen – im Browser bleibt der HTTP-Cache. Lösung: persistQueryClient in Kombination mit dem AsyncStorage-Persister speichert den Cache als JSON und stellt ihn beim Start wieder her. Verbindet man das mit networkMode: 'offlineFirst', liefert die App sofort gecachte Daten und versucht den Refetch im Hintergrund.

import AsyncStorage from '@react-native-async-storage/async-storage';
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';

const persister = createAsyncStoragePersister({
  storage: AsyncStorage,
  throttleTime: 1000,
});

export default function RootLayout() {
  return (
    <PersistQueryClientProvider
      client={client}
      persistOptions={{ persister, maxAge: 1000 * 60 * 60 * 24 * 7 }}
    >
      <Slot />
    </PersistQueryClientProvider>
  );
}

Für eine kontextuelle Tiefenintegration mit globalem Client-State – etwa Auth-Tokens, Theme oder UI-Flags – bietet sich Zustand als ergänzender Store an. Server-State und Client-State sauber zu trennen ist eine der wichtigsten Architektur-Entscheidungen, die ich im letzten Jahr in Reviews gesehen habe.

Infinite Queries und Pagination

Endlose Listen sind im Mobile-Kontext häufiger als auf dem Web – jeder Feed, jede Suche, jede Chat-History. useInfiniteQuery orchestriert Cursor-basiertes Paging und integriert sich direkt mit FlashList oder FlatList über onEndReached:

const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
  useInfiniteQuery({
    queryKey: ['feed'],
    queryFn: ({ pageParam }) =>
      fetch(`/api/feed?cursor=${pageParam}`).then(r => r.json()),
    initialPageParam: null as string | null,
    getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
  });

const items = data?.pages.flatMap(p => p.items) ?? [];

return (
  <FlashList
    data={items}
    onEndReached={() => hasNextPage && fetchNextPage()}
    onEndReachedThreshold={0.5}
    renderItem={({ item }) => <Row item={item} />}
  />
);

Für High-Performance-Listen mit Tausenden Items empfehle ich die Kombination mit FlashList v2 und der New Architecture – die Cell-Recycling-Strategie passt perfekt zu paginierten Daten, weil sich die gerenderten Komponenten typgleich wiederverwenden lassen.

TanStack Query vs. Zustand: Wann was?

Eine häufige Frage in meinen Code-Reviews: Brauche ich beides? Die kurze Antwort lautet ja, weil sie unterschiedliche Probleme lösen. Server-State hat einen Owner (das Backend), kann veralten, muss revalidiert werden und braucht Loading/Error-Zustände. Client-State (Theme, Filter-Auswahl, Modal offen) lebt nur im Memory der App.

DimensionTanStack Query 5Zustand 5
HauptzweckServer-State, asynchrone DatenClient-State, synchrone Werte
CachingEingebaut, mit Stale-TimeNicht vorhanden – manuell
PersistenzAsyncStorage-Persisterpersist-Middleware
Netzwerk-RefetchAutomatisch via onlineManagerNicht relevant
Bundle-Größe (gzip)~13 KB~1.1 KB
LernkurveMittel (Cache-Konzepte)Niedrig (Store + Selektoren)
EinsatzAPI-Layer, GraphQL, RESTUI-Flags, Auth-Token, Settings

Praxis-Faustregel: Wenn der Wert per HTTP gefetcht wird, gehört er in TanStack Query. Wenn er nur im JavaScript-Memory lebt, in Zustand (oder bei kleinen Scopes in useState). Beide miteinander zu kombinieren, ist explizit gewünscht – die Libraries treten sich nicht auf die Füße.

Debugging mit React Native DevTools

Seit React Native 0.79 (Mai 2025) sind die offiziellen React Native DevTools stabil und ersetzen Flipper komplett. Die @tanstack/react-query-devtools für Mobile sind seit v5.55 ebenfalls integriert: Sie laufen als floating Inspector, der den aktuellen Cache, Stale-Times und laufende Fetches in einer Liste zeigt.

import { DevToolsBubble } from '@tanstack/react-query-devtools';

export function AppRoot() {
  return (
    <>
      <Slot />
      {__DEV__ && <DevToolsBubble />}
    </>
  );
}

Für Animations- und Layout-Bugs in Listen, die nach Query-Updates auftreten, ist außerdem ein Blick auf Reanimated 4 und die neue CSS-Animations-API hilfreich – Refetches, die mit Layout-Animationen kollidieren, sind ein subtiles Performance-Problem.

Häufige Fragen

Funktioniert TanStack Query mit der New Architecture in React Native?

Ja. Seit v5.30 ist TanStack Query vollständig kompatibel mit Fabric, Turbo Modules und Hermes. Es gibt keine native Abhängigkeit – die Library läuft rein in JavaScript und ist daher New-Architecture-agnostisch.

Wie viel Speicher verbraucht der Cache?

Standardmäßig nur RAM, dimensioniert über gcTime (Default 5 Minuten). Mit dem AsyncStorage-Persister kommen je nach Daten 100 KB bis wenige MB Disk hinzu. Setze maxAge, um den persistierten Cache zu beschränken.

Brauche ich Axios, oder reicht fetch?

Fetch reicht für die meisten Apps. TanStack Query kümmert sich um Retry, Deduplizierung und Caching – das sind die Hauptgründe, die früher für Axios sprachen. Nutze Axios nur, wenn du Interceptoren für Auth oder spezifisches Error-Mapping brauchst.

Wie ersetze ich Redux Toolkit Query?

RTK Query und TanStack Query lösen dasselbe Problem. Migrations-Strategie: Ersetze Endpoint-Definitionen durch queryKey-basierte Hooks, lass Redux nur noch für Client-State stehen oder ersetze es ganz durch Zustand. Plane einen Endpunkt pro PR – Big-Bang-Migrationen scheitern verlässlich.

Kann ich TanStack Query mit GraphQL nutzen?

Ja. Die Library ist transport-agnostisch – jede asynchrone Funktion taugt als queryFn. Für GraphQL passt graphql-request gut; für komplexere Setups mit Subscriptions ist Apollo oder urql weiterhin die bessere Wahl.

Anita Iyer
Über den Autor Anita Iyer

Cross-platform mobile developer who came to RN from web. Bridges the two worlds and explains the seams.