TanStack Query (React Query) i React Native 2026: Data-fetching, caching og offline-support
Lær hvordan du bruger TanStack Query v5 i React Native: opsætning, useQuery, mutations, offline-cache med MMKV, samt integration med NetInfo og AppState.
TanStack Query (tidligere kendt som React Query) er et data-fetching-bibliotek der håndterer server-state, caching og synkronisering i React Native-apps, og det fungerer stort set identisk med web-versionen. Kommer du fra React på web, kan du genbruge næsten al din viden: useQuery, useMutation, QueryClient og cache-invalidering opfører sig ens på tværs af platforme. Det du skal lære er, hvordan du gifter det sammen med React Natives AppState, netværkslag og persistente storage som MMKV.
TanStack Query v5 fungerer 1:1 mellem React Web og React Native, med samme API og samme mentale model.
staleTime bestemmer hvornår data er "gammelt" og skal genhentes; gcTime (tidligere cacheTime) bestemmer hvornår ubrugte queries ryddes fra hukommelsen.
Persistér cachen med @tanstack/query-async-storage-persister eller MMKV for at få reel offline-support.
Brug onlineManager med NetInfo og focusManager med AppState for at pause og genstarte queries korrekt.
TanStack Query erstatter server-state i Redux eller Zustand, men ikke UI-state; de to lag lever fint side om side.
Optimistic updates og offline-mutations kræver en persistent mutation cache og en mutationDefaults-registrering.
Hvorfor TanStack Query passer perfekt i React Native
Da jeg først kom fra web til React Native, brugte jeg måneder på at genopfinde data-fetching manuelt: useState til data, useState til loading, useState til fejl, useEffect til at kalde fetch, og en eller anden global map til at cache resultater mellem skærme. Det virkede. Det var også helt unødvendigt, for TanStack Query løser præcis dét problem, og fordi det ikke afhænger af DOM eller browser-specifikke API'er, kører hele pakken uforandret på iOS og Android.
Konkret får du fire ting, der er svære at bygge selv i React Native:
Dedupliceret hentning. Hvis tre skærme monteres samtidig og alle beder om ['user', id], sker der ét netværkskald, ikke tre.
Automatisk refetching når appen kommer i forgrunden eller netværket kommer tilbage. Begge dele er ekstra vigtige på mobil, hvor apps ofte pausere.
Baggrunds-opdateringer, så brugeren aldrig ser en spinner, hvis der ligger friske data i cachen.
Stabil server-state adskilt fra UI-state. Zustand eller Redux slipper for at holde en cache af HTTP-svar, hvilket de aldrig var designet til.
Version 5 (aktuelt v5.62 i august 2026) er et krav for React Native. V4 havde flere subtile bugs omkring AppState og garbage collection af inaktive queries, som blev fikset som en del af den store API-omlægning. Læs de officielle React Native-anbefalinger fra TanStack Query før du bygger et større projekt.
Installation og opsætning i en Expo- eller bare-app
Installationen er identisk med web: én pakke, én provider øverst i træet. I en Expo-app (SDK 51+) eller et bare React Native-projekt kører du:
npx expo install @tanstack/react-query
# eller for et bare-projekt:
npm install @tanstack/react-query
Herefter opretter du én QueryClient pr. app og pakker roden ind i QueryClientProvider. Det er vigtigt at klienten oprettes uden for komponenten (eller med useState som init-funktion), ellers får du en ny klient og dermed en tom cache ved hver re-render:
// App.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useState } from 'react';
import RootNavigator from './src/navigation/RootNavigator';
export default function App() {
// Vigtigt: useState garanterer én stabil instans i komponentens levetid.
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60_000, // data er friske i 60 sek
gcTime: 5 * 60_000, // ubrugte queries ryddes efter 5 min
retry: 2, // 2 forsøg på netværksfejl
refetchOnWindowFocus: true, // fungerer via focusManager (se nedenfor)
},
},
})
);
return (
<QueryClientProvider client={queryClient}>
<RootNavigator />
</QueryClientProvider>
);
}
Det basale mønster er identisk med web. Du giver useQuery en query key (en serialiserbar array-nøgle) og en query function, og biblioteket klarer resten:
// hooks/useArticles.ts
import { useQuery } from '@tanstack/react-query';
type Article = { id: string; title: string; excerpt: string };
async function fetchArticles(): Promise<Article[]> {
const res = await fetch('https://api.example.com/articles');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
export function useArticles() {
return useQuery({
queryKey: ['articles'],
queryFn: fetchArticles,
staleTime: 2 * 60_000,
});
}
Så bruger du den i en skærm-komponent. Læg mærke til hvor lidt boilerplate der er tilbage sammenlignet med et manuelt useEffect:
Bemærk et par mobile-specifikke detaljer. Pull-to-refresh koblet direkte til refetch() giver en flydende native oplevelse; isRefetching (ikke isPending) driver spinner-tilstanden, så listen ikke tømmes hver gang. Og fordi TanStack Query cacher pr. queryKey, kan brugeren navigere væk og komme tilbage uden at se en tom skærm. Den viser cachet data med det samme, og henter i baggrunden hvis staleTime er overskredet.
Dynamiske queries med parametre
Nøglen skal inkludere alle inputs, for det er sådan cachen adskiller dem:
export function useArticle(id: string) {
return useQuery({
queryKey: ['article', id],
queryFn: () => fetch(`/api/articles/${id}`).then((r) => r.json()),
enabled: !!id, // spring queryen over hvis id endnu ikke er sat
});
}
Mutations, optimistic updates og invalidering
useMutation håndterer alt der ændrer server-state: POST, PUT, PATCH, DELETE. Det virker anderledes end useQuery; mutations kører ikke automatisk, du trigger dem med mutate() eller mutateAsync(). Det klassiske eksempel: like en artikel og opdatér UI'et med det samme.
import { useMutation, useQueryClient } from '@tanstack/react-query';
export function useLikeArticle() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (id: string) => {
const res = await fetch(`/api/articles/${id}/like`, { method: 'POST' });
if (!res.ok) throw new Error('like failed');
return res.json();
},
// 1) Optimistic update: vis ændringen i UI før serveren svarer
onMutate: async (id) => {
await qc.cancelQueries({ queryKey: ['article', id] });
const prev = qc.getQueryData(['article', id]);
qc.setQueryData(['article', id], (old: any) => ({
...old,
likes: (old?.likes ?? 0) + 1,
liked: true,
}));
return { prev };
},
// 2) Rul tilbage hvis serveren afviser
onError: (_err, id, context) => {
if (context?.prev) qc.setQueryData(['article', id], context.prev);
},
// 3) Synkronisér med serveren når mutationen er færdig
onSettled: (_data, _err, id) => {
qc.invalidateQueries({ queryKey: ['article', id] });
},
});
}
Denne firetrinsdans (cancel → snapshot → update → rollback/invalidate) er den samme du kender fra web, og den er lige så vigtig på mobil, hvor netværket ofte er langsomt eller upålideligt. Brugerens tryk på et hjerte skal føles øjeblikkeligt, uanset om det tager 50 ms eller 5 sekunder at nå backend.
Til bredere invalidering (fx når brugeren opretter en ny artikel og listen skal opdateres) bruger du queryClient.invalidateQueries({ queryKey: ['articles'] }). TanStack Query bruger prefix-matching, så ['articles'] matcher også ['articles', 'page', 2].
Offline-support: persistér cachen med MMKV eller AsyncStorage
Her adskiller React Native sig for alvor fra web. På web forsvinder cachen bare når fanen lukkes; på mobil forventer brugeren, at appen kan åbnes offline og stadig vise det seneste indhold. Løsningen er en persister der serialiserer cachen til disk.
Der er to fornuftige valg i 2026: react-native-mmkv (hurtigst, kræver JSI, perfekt til den nye arkitektur) eller @react-native-async-storage/async-storage (langsommere, men fungerer i alle miljøer inklusive Expo Go). Jeg bruger MMKV i produktion, fordi cachestørrelser over 1 MB bliver mærkbart tunge med AsyncStorage.
// App.tsx
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
import { MMKV } from 'react-native-mmkv';
import { QueryClient } from '@tanstack/react-query';
const storage = new MMKV({ id: 'query-cache' });
const mmkvPersister = createSyncStoragePersister({
storage: {
getItem: (k) => storage.getString(k) ?? null,
setItem: (k, v) => storage.set(k, v),
removeItem: (k) => storage.delete(k),
},
});
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 24 t: skal være ≥ persist maxAge
staleTime: 60_000,
},
},
});
export default function App() {
return (
<PersistQueryClientProvider
client={queryClient}
persistOptions={{
persister: mmkvPersister,
maxAge: 1000 * 60 * 60 * 24, // rehydrer kun cache < 24 t
buster: 'v1', // ændr for at nulstille cachen ved deploy
}}>
<RootNavigator />
</PersistQueryClientProvider>
);
}
Vil du også have offline-mutations (fx brugeren liker en artikel i flytilstand, og handlingen skal afspilles når nettet kommer tilbage), skal du registrere mutationDefaults med samme mutationKey som dine offline-mutationer bruger. Se sektionen om Persisted Mutations i den officielle dokumentation. Den er kort, men konfigurationen er ikke intuitiv første gang. Jeg brændte flere timer på præcis den ting i mit sidste projekt, før pengene faldt.
TanStack Query vs Zustand vs Redux: hvornår vælger du hvad?
Den vigtigste indsigt er, at de tre biblioteker ikke er konkurrenter. De løser forskellige problemer. TanStack Query er til server-state: data der ejes af en backend og skal caches, synkroniseres og invalideres. Zustand og Redux Toolkit er til klient-state: modal-tilstande, tema-valg, formularværdier, valgt fane osv.
Aspekt
TanStack Query v5
Zustand 5
Redux Toolkit 2
Primær use case
Server-state, HTTP-cache
Klient-state, small stores
Klient-state, store apps
Bundle-størrelse (gzip)
~13 KB
~1 KB
~12 KB
Boilerplate
Meget lav (hooks)
Meget lav
Middel (slices)
Indbygget caching
Ja
Nej
Kun via RTK Query
Offline-support
Ja (persister)
Manuelt
Manuelt / redux-persist
DevTools
Fremragende
Basic
Fremragende
Læringskurve
Lav-middel
Meget lav
Middel
I praksis: brug TanStack Query til alt hvad der kommer fra backend, og Zustand (eller useState/useReducer) til alt andet. Har du brug for en dybere sammenligning af klient-state-værktøjer, har vi skrevet en detaljeret gennemgang af Zustand vs Redux Toolkit i React Native.
React Native-specifikke tips: NetInfo, AppState og refetching
Dette er den del, hvor web-versionen og React Native-versionen adskiller sig. På web bruger TanStack Query window.addEventListener('online', ...) og document.visibilitychange til at vide, hvornår appen er online og synlig. Ingen af de events findes på mobil, så du skal koble dem manuelt.
Med denne registrering vil TanStack Query pause mutationer når enheden mister forbindelse, og automatisk afspille dem når nettet vender tilbage.
App-fokus via AppState
// setup/focusManager.ts
import { AppState, AppStateStatus, Platform } from 'react-native';
import { focusManager } from '@tanstack/react-query';
function onAppStateChange(status: AppStateStatus) {
if (Platform.OS !== 'web') {
focusManager.setFocused(status === 'active');
}
}
AppState.addEventListener('change', onAppStateChange);
Nu betyder refetchOnWindowFocus: true, at queries genopfriskes når brugeren skifter tilbage til appen fra multitasking. Kombinér det med korrekte staleTime-værdier for at undgå unødvendige netværkskald.
Almindelige fejl og hvordan du undgår dem
Efter to år med TanStack Query i produktion har jeg set (og lavet) de samme fejl gentagne gange. De vigtigste at kende:
At oprette QueryClient inde i komponenten uden useState. Hver re-render giver en ny klient og en tom cache. Brug altid const [qc] = useState(() => new QueryClient()) eller en modul-level konstant.
Ikke-serialiserbare query keys. Nøglen bruges som cache-identifikator og skal derfor være JSON-serialiserbar. Objekter er OK, men Date, Map, klasse-instanser eller funktioner går galt. Brug primitiver: ['user', userId, 'posts', pageIndex].
At bruge data uden at tjekke isPending. Første render har altid data === undefined. TypeScript fanger det hvis du har strict slået til; hvis ikke, får du en runtime-crash første gang skærmen mountes. Jeg hittede den bug live i produktion på en betaudgivelse, og det var ikke sjovt.
Manglende enabled-flag på afhængige queries. Hvis en query kræver et id der først kommer fra en anden query, sæt enabled: !!id, ellers kaldes den med undefined og fejler.
Global refetchInterval på 30 sekunder. Fristende, men det brænder batteri og data. Brug polling kun til queries hvor det giver forretningsmæssig mening, og pause via refetchIntervalInBackground: false.
Cache-persistens uden versionering. Skifter du skemaet for et responseobjekt uden at bumpe buster, får eksisterende brugere gammelt-format-data læst tilbage fra disk, og din app crasher. Sæt altid en buster-streng, og opdatér den ved brydende ændringer.
Ofte stillede spørgsmål
Virker TanStack Query overhovedet i React Native?
Ja. TanStack Query v5 er fuldt understøttet i React Native og har en dedikeret sektion i den officielle dokumentation. Kernen er platform-uafhængig, så useQuery, useMutation og QueryClient fungerer identisk. Du skal kun tilføje en integration med NetInfo og AppState for online-/fokus-detektering.
Hvad er forskellen mellem staleTime og gcTime?
staleTime bestemmer hvor længe data betragtes som friske og ikke skal genhentes; under den periode returneres cachen uden netværkskald. gcTime (garbage collection time) bestemmer hvor længe en inaktiv query beholdes i hukommelsen, før den ryddes. Default er 0 for staleTime og 5 minutter for gcTime.
Har jeg stadig brug for Redux hvis jeg bruger TanStack Query?
Sandsynligvis ikke, hvis dit "state" primært er data fra en backend. TanStack Query dækker server-state; til lille klient-state (tema, modal-åbne-tilstande) er Zustand eller bare useState nok. Redux bliver relevant igen, hvis du har komplekse UI-flows med mange tværgående actions og har brug for time-travel-debugging.
Hvordan persisterer jeg TanStack Query-cachen i React Native?
Installer @tanstack/react-query-persist-client og @tanstack/query-sync-storage-persister, og wrap din app i PersistQueryClientProvider. Brug react-native-mmkv som storage-adapter for bedst ydelse, eller AsyncStorage hvis du ikke kan bruge JSI. Sæt gcTime ≥ maxAge, ellers ryddes queries før de gemmes.
Kan TanStack Query håndtere offline-mutations?
Ja. Registrér onlineManager med NetInfo, så pauses mutationer automatisk når enheden er offline, og afspilles når nettet vender tilbage. For at persistere selve mutationerne (så de overlever en app-relaunch) skal du bruge en persister sammen med mutationDefaults-registreringer på din QueryClient.
Hvor stor er TanStack Query i bundle-størrelse?
Cirka 13 KB gzippet for @tanstack/react-query alene. Persister-pakken tilføjer ~3 KB, og NetInfo/MMKV kommer med native binaries, men er små JS-mæssigt. På en typisk React Native-bundle er det ubetydeligt sammenlignet med værdien.
Alt du behøver at vide om React Natives nye arkitektur (Fabric, TurboModules, JSI, Codegen) i 2026: hvad hver del gør, hvordan du aktiverer den i Expo SDK 52+, og migrationsstien for eksisterende apps.
Zustand vs Redux Toolkit i React Native 2026: praktisk sammenligning af bundle-størrelse, performance, DX og kodeeksempler, så du kan vælge det rigtige bibliotek til din app.