ApolloProvider, InMemoryCache, Optimistic UI und Offline-First
Apollo Client verbindet GraphQL und React Native über einen normalisierenden Cache, typisierte Hooks und eine Persistenzschicht, die Daten über App-Neustarts hinweg verfügbar hält. Wer ApolloClient, InMemoryCache und die Apollo-Hooks richtig konfiguriert, bekommt weniger Netzwerk-Roundtrips, sofortiges UI-Feedback durch Optimistic Updates und eine Anwendung, die auch bei instabiler Mobilfunkverbindung nutzbar bleibt.
Inhaltsverzeichnis
- 1. Warum GraphQL und Apollo Client zu React Native passen
- 2. ApolloClient und ApolloProvider: der Grundaufbau
- 3. InMemoryCache: Normalisierung und Konfiguration
- 4. Daten laden mit useQuery: Loading, Error, Data
- 5. useLazyQuery: Abfragen gezielt auslösen
- 6. Mutationen mit useMutation: Optimistic UI und Cache-Updates
- 7. Fehlerbehandlung: Netzwerkfehler vs. GraphQL-Fehler
- 8. Pagination mit fetchMore
- 9. Offline-First auf Mobile und Apollo Client im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum GraphQL und Apollo Client zu React Native passen
React Native Apps laufen selten in stabilen Netzwerkbedingungen. Wechsel zwischen WLAN und Mobilfunk, U-Bahn-Tunnel, Aufzüge und Auslandsroaming sorgen für schwankende Latenz und gelegentliche Verbindungsabbrüche. Genau in diesem Umfeld zeigt sich der Vorteil von GraphQL gegenüber klassischem REST: Statt für eine verschachtelte Ansicht, etwa ein Nutzerprofil mit Posts und Kommentaren, drei bis vier separate REST-Endpunkte nacheinander abzufragen, liefert eine einzige GraphQL-Query genau die benötigten Felder in einem einzigen Roundtrip. Auf einem Mobilfunknetz mit 150 bis 400 Millisekunden Latenz pro Request macht das den Unterschied zwischen einer Ansicht, die sofort wirkt, und einer, die sichtbar in Schüben nachlädt.
Apollo Client ist die Bibliothek, die diese GraphQL-Anfragen für React Native praktisch nutzbar macht. Sie beschränkt sich nicht auf das reine Absetzen von Queries, sondern bringt einen normalisierenden Cache, deduplizierte In-Flight-Requests und React-Hooks mit, die sich wie native Function-Component-Patterns anfühlen. Wer schon einmal manuell fetch-Aufrufe mit useState und useEffect für Loading- und Error-Zustände verdrahtet hat, merkt schnell, wie viel dieser Code durch useQuery und useMutation überflüssig wird.
Ein weiterer Grund liegt im typisierten Schema. GraphQL-Schemas sind ein verbindlicher Vertrag zwischen Backend und App, und Codegen-Werkzeuge erzeugen daraus TypeScript-Typen für jede Query und Mutation. Ein Tippfehler in einem Feldnamen wird so zur Kompilierzeit sichtbar, nicht erst als kaputte UI in Produktion. Bei React Native, wo App-Store-Reviews und OTA-Rollouts Release-Zyklen verlangsamen können, ist dieses frühe Erkennen von Fehlern besonders wertvoll, weil ein Hotfix nicht innerhalb von Minuten live sein kann wie bei einer Web-App.
2. ApolloClient und ApolloProvider: der Grundaufbau
Der Einstieg in Apollo Client beginnt mit drei Bausteinen: einem HttpLink, der die GraphQL-Endpoint-URL kennt, einer InMemoryCache-Instanz und dem ApolloClient-Konstruktor, der beides zusammenführt. Die ApolloProvider-Komponente wrapt danach die App-Wurzel und stellt den Client per React Context allen darunterliegenden Komponenten zur Verfügung, ohne dass er als Prop durch jede Ebene gereicht werden muss. In einer typischen Expo-App passiert das ganz oben in App.tsx, noch vor dem Navigation-Container.
Für Authentifizierung reicht ein einzelner HttpLink selten aus. Üblich ist eine Link-Kette mit ApolloLink.from, in der ein authLink per setContext das Access-Token aus dem sicheren Storage liest und als Authorization-Header anhängt, bevor die Anfrage den httpLink erreicht. Diese Kette lässt sich beliebig erweitern, etwa um einen errorLink für zentrales Logging oder einen retryLink für automatische Wiederholungen bei transienten Netzwerkfehlern.
Direkt im Setup lohnt sich bereits die Cache-Persistenz mit apollo3-cache-persist. Ein CachePersistor wird mit der InMemoryCache-Instanz und einem AsyncStorage-Wrapper erzeugt, und dessen restore-Methode muss vor dem ersten Rendern abgewartet werden. In Expo lässt sich das gut mit SplashScreen.preventAutoHideAsync kombinieren, damit die App nicht kurz einen leeren Cache-Zustand zeigt, bevor die persistierten Daten geladen sind.
# Install Apollo Client, GraphQL, and cache persistence for React Native
npm install @apollo/client graphql
npm install apollo3-cache-persist @react-native-async-storage/async-storage
# Optional: NetInfo to detect connectivity changes for offline-aware links
npm install @react-native-community/netinfo
// apolloClient.js - ApolloClient + ApolloProvider setup with persisted cache
import { useEffect, useState } from 'react';
import {
ApolloClient,
ApolloProvider,
InMemoryCache,
HttpLink,
ApolloLink,
} from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import { CachePersistor } from 'apollo3-cache-persist';
import AsyncStorage from '@react-native-async-storage/async-storage';
import * as SecureStore from 'expo-secure-store';
const httpLink = new HttpLink({
uri: 'https://api.example.com/graphql',
});
// Auth middleware: attach the access token to every outgoing request
const authLink = setContext(async (_, { headers }) => {
const token = await SecureStore.getItemAsync('access_token');
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : '',
},
};
});
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
feedItems: {
keyArgs: false,
merge(existing = [], incoming, { args }) {
// Append rather than overwrite so fetchMore extends the list
const offset = args?.offset ?? 0;
const merged = existing.slice(0);
for (let i = 0; i < incoming.length; i++) {
merged[offset + i] = incoming[i];
}
return merged;
},
},
},
},
},
});
export function ApolloAppProvider({ children }) {
const [client, setClient] = useState(null);
useEffect(() => {
async function init() {
const persistor = new CachePersistor({
cache,
storage: AsyncStorage,
maxSize: false,
debug: __DEV__,
});
// Restore persisted cache before the app renders any query result
await persistor.restore();
setClient(
new ApolloClient({
link: ApolloLink.from([authLink, httpLink]),
cache,
connectToDevTools: __DEV__,
})
);
}
init();
}, []);
if (!client) {
return null; // keep native splash screen visible until cache is restored
}
return <ApolloProvider client={client}>{children}</ApolloProvider>;
}
3. InMemoryCache: Normalisierung und Konfiguration
Der zentrale Mehrwert von Apollo Client gegenüber einem einfachen fetch-Wrapper liegt in der InMemoryCache und ihrer Normalisierung. Jedes Objekt, das ein Feld __typename und ein eindeutiges Identifikationsfeld wie id oder uid mitbringt, wird nicht als verschachtelte Kopie im Antwortbaum gespeichert, sondern als flacher Eintrag unter dem Schlüssel Typename:id abgelegt. Fragen zwei unterschiedliche Screens dasselbe Objekt ab, etwa ein Produkt in einer Liste und dasselbe Produkt auf einer Detailseite, zeigen beide dieselbe Cache-Referenz. Ändert eine Mutation dieses Objekt, aktualisieren sich beide Ansichten automatisch, ohne dass ein manueller Re-Fetch nötig ist.
Diese Normalisierung lässt sich über typePolicies feinjustieren. Objekte ohne Standard-id-Feld brauchen eine eigene keyFields-Definition, etwa eine Kombination aus zwei Feldern als zusammengesetzter Schlüssel. Für Listenfelder, die per Pagination wachsen, definiert eine merge-Funktion, wie neue Daten mit bereits vorhandenen kombiniert werden, statt sie einfach zu überschreiben. Ohne eine solche merge-Funktion würde jeder fetchMore-Aufruf die bisherige Liste durch die neue Seite ersetzen, statt sie zu erweitern.
Auf React Native zahlt sich dieser normalisierte Cache doppelt aus. Erstens reduziert er Netzwerk-Traffic, was bei begrenztem mobilem Datenvolumen direkt spürbar ist. Zweitens ermöglicht er sofortige UI-Darstellung beim Zurück-Navigieren zwischen Screens im React-Navigation-Stack, weil useQuery bei gleicher Query und gleichen Variablen zuerst die gecachten Daten zeigt, bevor im Hintergrund ein Refetch entscheidet, ob sich etwas geändert hat.
// schema.graphql (excerpt) - id and __typename drive cache normalization
type Product {
id: ID!
name: String!
price: Float!
inStock: Boolean!
}
type ProductConnection {
edges: [ProductEdge!]!
pageInfo: PageInfo!
}
type ProductEdge {
cursor: String!
node: Product!
}
type PageInfo {
hasNextPage: Boolean!
endCursor: String
}
type Query {
products(first: Int!, after: String): ProductConnection!
}
4. Daten laden mit useQuery: Loading, Error, Data
Der useQuery-Hook ist der häufigste Einstiegspunkt in Apollo Client und führt eine GraphQL-Query beim Mounten der Komponente automatisch aus. Er liefert ein Objekt mit loading, error, data sowie Hilfsfunktionen wie refetch und fetchMore zurück. Das übliche Pattern in React Native-Komponenten ist ein frühes Return für den Loading- und Error-Zustand, bevor der eigentliche Content mit den geladenen Daten gerendert wird. Das hält die JSX lesbar und vermeidet verschachtelte Bedingungen tief im Baum.
Über die options von useQuery lassen sich fetchPolicy, skip und variables steuern. Eine fetchPolicy von cache-and-network zeigt sofort gecachte Daten, löst aber parallel einen Netzwerk-Request aus, während cache-first einen Request nur dann feuert, wenn im Cache noch nichts vorliegt. Auf React Native ist die Kombination mit useFocusEffect aus React Navigation sinnvoll: Kommt ein Nutzer per Zurück-Geste auf einen Screen zurück, kann ein gezielter refetch veraltete Daten aktualisieren, ohne dass die Query beim reinen Wechsel des Tabs erneut komplett neu lädt.
Für Pull-to-refresh-Verhalten, das auf Mobile fast erwartet wird, reicht die RefreshControl-Komponente aus React Native in Kombination mit der refetch-Funktion von useQuery. Ein lokaler refreshing-State steuert die Anzeige des Spinners, während refetch im Hintergrund dieselbe Query mit denselben Variablen erneut ausführt und den Cache aktualisiert, sobald die Antwort eintrifft.
// ProductListScreen.jsx - useQuery with explicit loading/error/data handling
import { FlatList, Text, View, RefreshControl } from 'react-native';
import { useQuery, gql } from '@apollo/client';
import { useState, useCallback } from 'react';
const PRODUCTS_QUERY = gql`
query Products($first: Int!, $after: String) {
products(first: $first, after: $after) {
edges {
cursor
node {
id
name
price
inStock
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
`;
export function ProductListScreen() {
const [refreshing, setRefreshing] = useState(false);
const { loading, error, data, refetch } = useQuery(PRODUCTS_QUERY, {
variables: { first: 20, after: null },
fetchPolicy: 'cache-and-network',
notifyOnNetworkStatusChange: true,
});
const onRefresh = useCallback(async () => {
setRefreshing(true);
await refetch();
setRefreshing(false);
}, [refetch]);
if (loading && !data) {
return <Text>Loading products...</Text>;
}
if (error) {
return <Text>Could not load products: {error.message}</Text>;
}
const products = data.products.edges.map((edge) => edge.node);
return (
<FlatList
data={products}
keyExtractor={(item) => item.id}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
renderItem={({ item }) => (
<View style={{ padding: 12 }}>
<Text style={{ fontWeight: 'bold' }}>{item.name}</Text>
<Text>{item.inStock ? `$${item.price}` : 'Out of stock'}</Text>
</View>
)}
/>
);
}
5. useLazyQuery: Abfragen gezielt auslösen
Während useQuery eine Query sofort beim Mounten der Komponente feuert, wartet useLazyQuery auf einen expliziten Trigger. Der Hook liefert kein einfaches Objekt zurück, sondern ein Tuple: eine Trigger-Funktion an erster Stelle und ein Ergebnis-Objekt mit loading, data und error an zweiter Stelle. Dieses Muster passt zu Interaktionen, bei denen der Zeitpunkt des Requests vom Nutzer bestimmt wird, nicht vom Rendern der Komponente.
Typische Anwendungsfälle in React Native sind ein Suchfeld mit Debounce, das erst nach einer Tippunterbrechung von einigen hundert Millisekunden eine GraphQL-Query abschickt, oder ein Barcode-Scanner-Screen, der eine Produkt-Query erst nach erfolgreichem Scan auslöst. Auch ein Login-Formular profitiert davon: Die Query zur Profilabfrage soll erst nach erfolgreicher Authentifizierung laufen, nicht schon beim Rendern des Formulars.
Ein wichtiger Unterschied zu useQuery betrifft den Cache: useLazyQuery nutzt denselben normalisierten InMemoryCache und respektiert dieselbe fetchPolicy, ausgelöst wird der Request aber ausschließlich durch den manuellen Aufruf der Trigger-Funktion. Wiederholte Aufrufe mit denselben Variablen greifen wie bei useQuery zuerst auf den Cache zu, bevor ein neuer Netzwerk-Request entschieden wird.
6. Mutationen mit useMutation: Optimistic UI und Cache-Updates
Der useMutation-Hook führt Schreiboperationen aus und liefert eine Trigger-Funktion zusammen mit loading-, error- und data-Zuständen. Der wichtigste Hebel für gefühlte Performance ist die optimisticResponse-Option: Statt auf die Server-Antwort zu warten, aktualisiert Apollo Client den Cache sofort mit dem erwarteten Ergebnis, und die UI zeigt die Änderung, noch bevor ein einziges Byte vom Server zurückkommt. Auf Mobilfunknetzen mit 200 bis 800 Millisekunden Latenz pro Request macht das den Unterschied zwischen einer App, die träge wirkt, und einer, die sich nativ schnell anfühlt.
Für Fälle, in denen reine Normalisierung nicht ausreicht, etwa das Einfügen eines neuen Elements in eine Liste, bietet die update-Funktion Zugriff auf cache.modify. Darüber lässt sich ein bestehendes Cache-Feld gezielt lesen und mit dem neuen Element erweitern, ohne die gesamte Query erneut auszuführen. Das ist besonders bei Listen mit Pagination relevant, wo ein einfaches Refetch teuer wäre und den Scroll-Zustand des Nutzers zerstören würde.
Schlägt die Mutation tatsächlich fehl, entfernt Apollo Client die optimistische Schicht automatisch und stellt den vorherigen Cache-Zustand wieder her. Die UI zeigt kurzzeitig die optimistische Version, korrigiert sich dann aber selbstständig, sobald der echte Fehler ankommt. Anwendungscode sollte diesen Fehlerfall trotzdem explizit behandeln, etwa mit einem Toast, der den Nutzer über das Scheitern informiert, weil ein stiller Rollback ohne Hinweis verwirrend wirkt.
// AddToCartButton.jsx - useMutation with optimistic response and cache.modify
import { Pressable, Text } from 'react-native';
import { useMutation, gql } from '@apollo/client';
const ADD_TO_CART = gql`
mutation AddToCart($productId: ID!, $quantity: Int!) {
addToCart(productId: $productId, quantity: $quantity) {
id
quantity
product {
id
name
}
}
}
`;
export function AddToCartButton({ product }) {
const [addToCart, { loading }] = useMutation(ADD_TO_CART, {
variables: { productId: product.id, quantity: 1 },
// UI updates immediately, before the server responds
optimisticResponse: {
addToCart: {
__typename: 'CartItem',
id: `optimistic-${product.id}`,
quantity: 1,
product: {
__typename: 'Product',
id: product.id,
name: product.name,
},
},
},
update(cache, { data }) {
const newItem = data.addToCart;
cache.modify({
fields: {
cartItems(existingItems = []) {
const newItemRef = cache.writeFragment({
data: newItem,
fragment: gql`
fragment NewCartItem on CartItem {
id
quantity
product {
id
name
}
}
`,
});
return [...existingItems, newItemRef];
},
},
});
},
onError(error) {
// Apollo Client already rolled the optimistic layer back automatically
console.warn('Add to cart failed:', error.message);
},
});
return (
<Pressable onPress={() => addToCart()} disabled={loading}>
<Text>{loading ? 'Adding...' : 'Add to cart'}</Text>
</Pressable>
);
}
7. Fehlerbehandlung: Netzwerkfehler vs. GraphQL-Fehler
Ein ApolloError-Objekt unterscheidet zwei grundlegend verschiedene Fehlerkategorien, und diese Unterscheidung ist für eine saubere React Native-UI entscheidend. Ein networkError bedeutet, dass die Anfrage den Server gar nicht erreicht oder keine Antwort erhalten hat, etwa bei fehlender Verbindung, einem DNS-Fehler oder einem Timeout. Ein graphQLErrors-Array dagegen bedeutet, dass der Server geantwortet hat, die Anfrage aber inhaltlich abgelehnt wurde, etwa wegen fehlender Berechtigung, einer Validierungsregel oder eines nicht gefundenen Datensatzes.
Auf Mobile ist der networkError-Fall besonders häufig, weil Flugmodus, Funklöcher und U-Bahn-Tunnel zum Alltag gehören. Hier ist eine spezifische UX sinnvoll, etwa eine dezente Snackbar mit "Keine Verbindung" statt einer generischen Fehlermeldung, die den Nutzer verunsichert. graphQLErrors lassen sich dagegen oft feiner behandeln: Ein Fehlercode wie AUTH_NOT_AUTHENTICATED in den extensions eines GraphQL-Fehlers kann automatisch zu einem Redirect auf den Login-Screen führen, während ein Validierungsfehler direkt am betroffenen Formularfeld angezeigt wird.
Statt jede Komponente einzeln mit try-catch abzusichern, lohnt sich ein zentraler errorLink in der Apollo-Link-Kette. Dort lassen sich alle Fehler an einen Logging-Dienst wie Sentry weiterreichen, bevor sie überhaupt eine Komponente erreichen. Ein zusätzlicher retryLink kann transiente Netzwerkfehler automatisch mit exponentiellem Backoff wiederholen, sodass ein kurzer Verbindungsabbruch im U-Bahn-Tunnel gar nicht erst als sichtbarer Fehler in der UI ankommt, sondern der zweite oder dritte Versuch bereits erfolgreich ist.
8. Pagination mit fetchMore
Die fetchMore-Funktion, die useQuery zurückgibt, lädt zusätzliche Daten mit veränderten Variablen nach, etwa einem neuen after-Cursor, und kombiniert das Ergebnis über die im Cache definierte merge-Funktion mit den bereits geladenen Daten. In aktuellen Versionen von Apollo Client ist das die empfohlene Vorgehensweise, während die früher genutzte updateQuery-Option inzwischen als veraltet gilt zugunsten der zentralen merge-Logik in den typePolicies.
Für die Struktur der Pagination selbst empfiehlt sich das Cursor-basierte Relay-Connection-Pattern mit edges, node, pageInfo, hasNextPage und endCursor, wie es im Schema-Beispiel aus Abschnitt 3 bereits angelegt ist. Cursor-basierte Pagination ist robuster als reine offset-basierte Pagination, weil sie auch dann konsistent bleibt, wenn sich die zugrunde liegende Liste zwischen zwei Requests ändert, etwa weil ein Element gelöscht wurde.
In der UI löst ein onEndReached-Callback von FlatList den nächsten fetchMore-Aufruf aus, sobald der Nutzer sich dem Ende der aktuell geladenen Liste nähert. Wichtig ist ein Guard gegen doppelte Aufrufe: Ohne eine Prüfung des aktuellen loading-Zustands kann schnelles Scrollen mehrere fetchMore-Aufrufe gleichzeitig auslösen und unnötige Duplikat-Requests erzeugen. Ein einfacher Aufruf sieht so aus: fetchMore(variables: after data.products.pageInfo.endCursor), kombiniert mit einem Footer-Loading-Indikator, solange die nächste Seite lädt.
9. Offline-First auf Mobile und Apollo Client im Vergleich
Mobile Realität bedeutet, dass eine Netzwerkverbindung jederzeit abbrechen kann, während eine App gleichzeitig zwischen Vordergrund und Hintergrund wechselt, wenn der Nutzer zu einer anderen App wechselt oder das Gerät sperrt. Apollo Client ist von Haus aus kein vollständiges Offline-Queue-System, aber die Kombination aus Cache-Persistenz, sauberer Fehlerbehandlung und AppState-Handling deckt die meisten praktischen Fälle einer React Native-App ab.
Der bereits gezeigte CachePersistor sorgt dafür, dass beim App-Start sofort gecachte Daten aus AsyncStorage verfügbar sind, noch bevor eine einzige Netzwerkanfrage beantwortet wurde. Ergänzend lohnt sich ein AppState-Listener, der beim Wechsel in den Zustand active die Funktion client.reFetchObservableQueries aufruft, damit nach einer längeren Zeit im Hintergrund veraltete Daten aktualisiert werden. In Kombination mit @react-native-community/netinfo lässt sich außerdem ein Link konfigurieren, der Mutationen bei fehlender Verbindung zurückhält, statt sie sofort scheitern zu lassen.
Im Vergleich zu anderen Ansätzen für GraphQL und Datenzugriff auf Mobile zeigt sich, warum Apollo Client für komplexere Apps oft die erste Wahl ist, während leichtere Alternativen für einfachere Anforderungen ausreichen können.
| Ansatz | Cache-Normalisierung | Bundle-Größe | Empfehlung für React Native |
|---|---|---|---|
| Apollo Client | Vollständig, normalisiert nach __typename+id | Größer, dafür komplettes Ökosystem | Komplexe Apps mit vielen verknüpften Entitäten |
| urql | Optional, über Graphcache-Erweiterung | Deutlich kleiner als Apollo Client | Kleinere Apps, wenn volle Normalisierung nicht nötig ist |
| REST + fetch | Kein eingebauter Cache | Minimal, keine zusätzliche Bibliothek | Nur bei sehr einfachen, wenigen Endpunkten sinnvoll |
| TanStack Query | Query-Key-basiert, kein GraphQL-Objekt-Normalisieren | Klein, datenquellen-agnostisch | Gut für REST, für GraphQL ohne Normalisierung schwächer |
10. Zusammenfassung
Die Kombination aus GraphQL und Apollo Client löst für React Native-Apps ein sehr konkretes Problem: verschachtelte Daten in einem einzigen Roundtrip laden, statt mehrere REST-Endpunkte nacheinander abzufragen. ApolloClient und ApolloProvider bilden das Grundgerüst, InMemoryCache normalisiert Objekte über __typename und id, und apollo3-cache-persist hält diesen Cache über App-Neustarts hinweg in AsyncStorage verfügbar. useQuery, useLazyQuery und useMutation decken die drei häufigsten Interaktionsmuster ab: automatisches Laden, nutzergesteuertes Laden und Schreiboperationen mit sofortigem UI-Feedback durch Optimistic Responses.
Für eine robuste mobile Erfahrung zählen zusätzlich die Details: die klare Trennung zwischen networkError und graphQLErrors für passende Fehlermeldungen, cursor-basierte Pagination mit fetchMore für wachsende Listen, und ein AppState-Listener, der beim Zurückkehren aus dem Hintergrund veraltete Daten aktualisiert. Wer diese Bausteine konsequent zusammensetzt, bekommt mit Apollo Client eine React Native-App, die sich auch bei schwankender Mobilfunkverbindung zuverlässig und schnell anfühlt.
React Native GraphQL-Integration mit Apollo Client, das Wichtigste auf einen Blick
Setup
ApolloClient mit HttpLink und InMemoryCache, ApolloProvider wrapt die App-Wurzel, authLink hängt Tokens per setContext an jeden Request.
Cache und Persistenz
InMemoryCache normalisiert über __typename+id, apollo3-cache-persist sichert den Cache in AsyncStorage über App-Neustarts hinweg.
Hooks
useQuery lädt automatisch, useLazyQuery auf Trigger, useMutation mit optimisticResponse für sofortiges UI-Feedback.
Mobile Robustheit
networkError vs. graphQLErrors sauber trennen, fetchMore für Pagination, AppState-Listener für Refetch nach Hintergrund-Phasen.