State Management in React Native 2026: Zustand en TanStack Query Combineren voor Schaalbare Apps

Ontdek hoe je Zustand en TanStack Query combineert voor moderne state management in React Native. Inclusief MMKV-persistentie, offline-first patronen en werkende TypeScript-voorbeelden.

React Native State: Zustand & TanStack Gids 2026

State management — als je er niet bij stilstaat, merk je het nauwelijks. Maar kies je de verkeerde aanpak? Dan zit je maandenlang vast aan trage re-renders, mysterieuze bugs die alleen op bepaalde momenten opduiken, en code waar niemand meer aan durft te komen. Klinkt dat herkenbaar? In 2026 is het landschap gelukkig behoorlijk opgeschud. De tijd van één grote state-library die álles moest doen is echt voorbij. De moderne aanpak draait om een simpel maar krachtig principe: scheid server state van client state. En eerlijk gezegd is de winnende combinatie daarvoor best duidelijk geworden — Zustand voor je client state, TanStack Query voor server state.

In deze gids laat ik je stap voor stap zien hoe je deze twee libraries combineert in een React Native-project. We bouwen een complete architectuur op, inclusief MMKV-persistentie, offline-ondersteuning en TypeScript-integratie. Of je nou al jaren met Redux werkt of voor het eerst met state management bezig bent: aan het einde van dit artikel heb je een setup die je direct in productie kunt gebruiken.

Waarom twee libraries in plaats van één?

De grootste fout die ik bij React Native-ontwikkelaars zie — en eerlijk gezegd heb ik 'm zelf ook gemaakt — is het opslaan van API-data in je client state store. Je fetcht data van een server, stopt het in Redux of Zustand, en probeert het vervolgens handmatig in sync te houden. Het resultaat? Stale data, race conditions en synchronisatielogica die na een paar weken niemand meer snapt.

De oplossing is eigenlijk verrassend simpel: gebruik het juiste gereedschap voor elk type state.

  • Server state — data die van een API komt en gedeeld wordt met andere gebruikers (producten, gebruikersprofielen, berichten). Hier gebruik je TanStack Query.
  • Client state — data die alleen op het apparaat van de gebruiker leeft (thema-voorkeur, winkelwagen, formulierstatus, navigatiestate). Hier gebruik je Zustand.
  • Lokale UI state — data die alleen binnen één component bestaat (modal open/dicht, dropdown-selectie). Gewoon useState, niks meer.

Zodra je dit onderscheid eenmaal doorhebt, vallen veel architectuurkeuzes vanzelf op hun plek.

Zustand v5: lichtgewicht client state

Zustand (Duits voor "toestand" — leuke trivia voor op feestjes) is in 2026 de populairste React state management library, met meer dan 20 miljoen wekelijkse npm-downloads. De huidige versie is v5.0 en weegt slechts ~1,2 KB. Wat het zo fijn maakt: je hebt geen Provider nodig, de API is minimaal, en het werkt gewoon lekker met React Native. Geen gedoe.

Je eerste Zustand-store

Laten we beginnen met het opzetten van een Zustand-store voor een e-commerce app. Zo ziet dat eruit:

// Installatie
// npm install zustand

// stores/useWinkelwagenStore.ts
import { create } from 'zustand';

interface Product {
  id: string;
  naam: string;
  prijs: number;
  afbeelding: string;
}

interface WinkelwagenItem extends Product {
  aantal: number;
}

interface WinkelwagenState {
  items: WinkelwagenItem[];
  voegToe: (product: Product) => void;
  verwijder: (productId: string) => void;
  verhoogAantal: (productId: string) => void;
  verlaagAantal: (productId: string) => void;
  leegWinkelwagen: () => void;
  totaalPrijs: () => number;
}

export const useWinkelwagenStore = create<WinkelwagenState>((set, get) => ({
  items: [],

  voegToe: (product) =>
    set((state) => {
      const bestaand = state.items.find((i) => i.id === product.id);
      if (bestaand) {
        return {
          items: state.items.map((i) =>
            i.id === product.id ? { ...i, aantal: i.aantal + 1 } : i
          ),
        };
      }
      return { items: [...state.items, { ...product, aantal: 1 }] };
    }),

  verwijder: (productId) =>
    set((state) => ({
      items: state.items.filter((i) => i.id !== productId),
    })),

  verhoogAantal: (productId) =>
    set((state) => ({
      items: state.items.map((i) =>
        i.id === productId ? { ...i, aantal: i.aantal + 1 } : i
      ),
    })),

  verlaagAantal: (productId) =>
    set((state) => ({
      items: state.items.map((i) =>
        i.id === productId && i.aantal > 1
          ? { ...i, aantal: i.aantal - 1 }
          : i
      ),
    })),

  leegWinkelwagen: () => set({ items: [] }),

  totaalPrijs: () =>
    get().items.reduce((totaal, item) => totaal + item.prijs * item.aantal, 0),
}));

Zustand gebruiken in componenten

Het mooie van Zustand is dat je met selectors precies kunt aangeven welke stukken state een component nodig heeft. Hierdoor re-rendert een component alleen wanneer die specifieke data verandert — niet bij iedere willekeurige state-update. Dat maakt een enorm verschil in de performance van je app.

// components/WinkelwagenBadge.tsx
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { useWinkelwagenStore } from '../stores/useWinkelwagenStore';

export function WinkelwagenBadge() {
  // Alleen het aantal items — component re-rendert NIET bij prijswijzigingen
  const aantalItems = useWinkelwagenStore(
    (state) => state.items.reduce((sum, i) => sum + i.aantal, 0)
  );

  if (aantalItems === 0) return null;

  return (
    <View style={styles.badge}>
      <Text style={styles.tekst}>{aantalItems}</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  badge: {
    backgroundColor: '#FF3B30',
    borderRadius: 10,
    minWidth: 20,
    height: 20,
    justifyContent: 'center',
    alignItems: 'center',
    paddingHorizontal: 6,
  },
  tekst: {
    color: '#fff',
    fontSize: 12,
    fontWeight: 'bold',
  },
});

TanStack Query v5: server state beheren

Goed, nu het andere stuk van de puzzel. TanStack Query (voorheen React Query) is in 2026 dé standaard voor server state in React Native. De huidige versie is v5.99 en biedt automatische caching, achtergrond-refetching, stale-time management en optimistic updates. Allemaal out of the box, zonder dat je daar zelf iets voor hoeft te bouwen.

TanStack Query opzetten voor React Native

React Native vereist een paar extra stappen vergeleken met een web-app. Je moet namelijk handmatig de netwerk- en focusstatus koppelen — iets wat in de browser automatisch gaat maar op mobiel niet. Niet moeilijk, maar wel belangrijk:

// Installatie
// npm install @tanstack/react-query

// providers/QueryProvider.tsx
import React from 'react';
import { AppState, Platform } from 'react-native';
import NetInfo from '@react-native-community/netinfo';
import {
  QueryClient,
  QueryClientProvider,
  focusManager,
  onlineManager,
} from '@tanstack/react-query';

// Stap 1: Netwerk-status koppelen aan TanStack Query
onlineManager.setEventListener((setOnline) => {
  return NetInfo.addEventListener((state) => {
    setOnline(!!state.isConnected);
  });
});

// Stap 2: App-focus koppelen (refetch bij terugkeer naar app)
focusManager.setEventListener((setFocused) => {
  const subscription = AppState.addEventListener('change', (status) => {
    if (Platform.OS !== 'web') {
      setFocused(status === 'active');
    }
  });
  return () => subscription.remove();
});

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5, // 5 minuten
      gcTime: 1000 * 60 * 30,   // 30 minuten garbage collection
      retry: 2,
      refetchOnWindowFocus: true,
      refetchOnReconnect: true,
    },
  },
});

export function QueryProvider({ children }: { children: React.ReactNode }) {
  return (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  );
}

Data ophalen met useQuery

Met TanStack Query schrijf je custom hooks die je overal in je app kunt hergebruiken. De library handelt caching, loading states en errors volledig automatisch af. Dat scheelt je een hoop boilerplate:

// hooks/useProducten.ts
import { useQuery } from '@tanstack/react-query';

interface Product {
  id: string;
  naam: string;
  prijs: number;
  afbeelding: string;
  categorie: string;
}

async function fetchProducten(categorie?: string): Promise<Product[]> {
  const url = categorie
    ? `https://api.example.com/producten?categorie=${categorie}`
    : 'https://api.example.com/producten';

  const response = await fetch(url);
  if (!response.ok) {
    throw new Error('Producten ophalen mislukt');
  }
  return response.json();
}

export function useProducten(categorie?: string) {
  return useQuery({
    queryKey: ['producten', { categorie }],
    queryFn: () => fetchProducten(categorie),
    staleTime: 1000 * 60 * 10, // 10 minuten vers
  });
}

// Gebruik in component:
// const { data: producten, isLoading, error } = useProducten('elektronica');

Data wijzigen met useMutation

Voor het aanmaken, wijzigen of verwijderen van server-data gebruik je useMutation in combinatie met query invalidation. Het idee is simpel: na een succesvolle mutatie vertel je TanStack Query welke queries opnieuw opgehaald moeten worden.

// hooks/useBestelling.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';

interface NieuweBestelling {
  items: { productId: string; aantal: number }[];
  verzendAdres: string;
}

export function usePlaatsBestelling() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (bestelling: NieuweBestelling) => {
      const response = await fetch('https://api.example.com/bestellingen', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(bestelling),
      });
      if (!response.ok) throw new Error('Bestelling plaatsen mislukt');
      return response.json();
    },
    onSuccess: () => {
      // Invalideer gerelateerde queries zodat ze opnieuw worden opgehaald
      queryClient.invalidateQueries({ queryKey: ['bestellingen'] });
      queryClient.invalidateQueries({ queryKey: ['producten'] });
    },
  });
}

De complete architectuur: Zustand + TanStack Query

Oké, nu wordt het pas echt interessant. Je begrijpt beide libraries — maar hoe laat je ze samenwerken in een echt project? Het kernprincipe is simpel: TanStack Query beheert alles wat van de server komt, Zustand beheert alles wat puur client-side is. Geen overlap, geen dubbele bronnen van waarheid.

// Aanbevolen projectstructuur:
// ├── src/
// │   ├── stores/           ← Zustand stores (client state)
// │   │   ├── useWinkelwagenStore.ts
// │   │   ├── useThemaStore.ts
// │   │   └── useAuthStore.ts
// │   ├── hooks/            ← TanStack Query hooks (server state)
// │   │   ├── useProducten.ts
// │   │   ├── useBestellingen.ts
// │   │   └── useGebruiker.ts
// │   ├── providers/
// │   │   └── QueryProvider.tsx
// │   └── api/              ← API-functies
// │       └── client.ts

Hier is een praktijkvoorbeeld van hoe de twee libraries samenwerken in een productdetailscherm. Let op hoe server state en client state naast elkaar bestaan zonder elkaar in de weg te zitten:

// screens/ProductDetailScherm.tsx
import React from 'react';
import {
  View,
  Text,
  Image,
  TouchableOpacity,
  ActivityIndicator,
  StyleSheet,
  Alert,
} from 'react-native';
import { useProducten } from '../hooks/useProducten';
import { useWinkelwagenStore } from '../stores/useWinkelwagenStore';

export function ProductDetailScherm({ route }) {
  const { productId } = route.params;

  // Server state via TanStack Query
  const { data: producten, isLoading, error } = useProducten();
  const product = producten?.find((p) => p.id === productId);

  // Client state via Zustand
  const voegToe = useWinkelwagenStore((state) => state.voegToe);
  const items = useWinkelwagenStore((state) => state.items);
  const inWinkelwagen = items.some((i) => i.id === productId);

  if (isLoading) {
    return (
      <View style={styles.center}>
        <ActivityIndicator size="large" color="#007AFF" />
      </View>
    );
  }

  if (error || !product) {
    return (
      <View style={styles.center}>
        <Text style={styles.fout}>Product niet gevonden</Text>
      </View>
    );
  }

  return (
    <View style={styles.container}>
      <Image source={{ uri: product.afbeelding }} style={styles.afbeelding} />
      <Text style={styles.naam}>{product.naam}</Text>
      <Text style={styles.prijs}>€{product.prijs.toFixed(2)}</Text>

      <TouchableOpacity
        style={[styles.knop, inWinkelwagen && styles.knopUitgeschakeld]}
        onPress={() => {
          voegToe(product);
          Alert.alert('Toegevoegd', `${product.naam} is toegevoegd aan je winkelwagen`);
        }}
        disabled={inWinkelwagen}
      >
        <Text style={styles.knopTekst}>
          {inWinkelwagen ? 'In winkelwagen' : 'Toevoegen aan winkelwagen'}
        </Text>
      </TouchableOpacity>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 16, backgroundColor: '#fff' },
  center: { flex: 1, justifyContent: 'center', alignItems: 'center' },
  afbeelding: { width: '100%', height: 300, borderRadius: 12 },
  naam: { fontSize: 24, fontWeight: 'bold', marginTop: 16 },
  prijs: { fontSize: 20, color: '#007AFF', marginTop: 8 },
  knop: {
    backgroundColor: '#007AFF',
    paddingVertical: 14,
    borderRadius: 10,
    alignItems: 'center',
    marginTop: 24,
  },
  knopUitgeschakeld: { backgroundColor: '#ccc' },
  knopTekst: { color: '#fff', fontSize: 16, fontWeight: '600' },
  fout: { fontSize: 16, color: '#FF3B30' },
});

MMKV-persistentie: snelle opslag voor Zustand

Standaard gaat al je Zustand-state verloren zodra de app wordt afgesloten. Best vervelend als je gebruiker net tien items in z'n winkelwagen had liggen. Voor dit soort data — winkelwagen-inhoud, gebruikersvoorkeuren, authenticatietokens — wil je persistentie. En de snelste optie in React Native is zonder twijfel MMKV: een key-value store die tot 30x sneller is dan AsyncStorage.

react-native-mmkv v4.3 is tegenwoordig een Nitro Module en vereist de Nieuwe Architectuur. Zo stel je het in met Zustand:

// Installatie
// npm install react-native-mmkv react-native-nitro-modules zustand-mmkv-storage

// stores/useVoorkeurenStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { mmkvStorage } from 'zustand-mmkv-storage';

interface VoorkeurenState {
  thema: 'licht' | 'donker' | 'systeem';
  taal: string;
  meldingen: boolean;
  setThema: (thema: 'licht' | 'donker' | 'systeem') => void;
  setTaal: (taal: string) => void;
  toggleMeldingen: () => void;
}

export const useVoorkeurenStore = create<VoorkeurenState>()(
  persist(
    (set) => ({
      thema: 'systeem',
      taal: 'nl',
      meldingen: true,

      setThema: (thema) => set({ thema }),
      setTaal: (taal) => set({ taal }),
      toggleMeldingen: () =>
        set((state) => ({ meldingen: !state.meldingen })),
    }),
    {
      name: 'gebruiker-voorkeuren',
      storage: createJSONStorage(() => mmkvStorage),
      // Sla alleen de data op, niet de functies
      partialize: (state) => ({
        thema: state.thema,
        taal: state.taal,
        meldingen: state.meldingen,
      }),
    }
  )
);

Hydratatie afhandelen

Bij het opstarten van de app moet Zustand eerst de opgeslagen state uit MMKV laden — dat heet hydrateren. Tot dat klaar is, wil je voorkomen dat de app even de standaardwaarden laat zien (de zogenaamde "flash of initial state"). Gelukkig is dat vrij makkelijk op te lossen:

// hooks/useHydratatie.ts
import { useEffect, useState } from 'react';
import { useVoorkeurenStore } from '../stores/useVoorkeurenStore';

export function useHydratatie() {
  const [isKlaar, setIsKlaar] = useState(false);

  useEffect(() => {
    // Zustand persist onFinishHydration callback
    const unsub = useVoorkeurenStore.persist.onFinishHydration(() => {
      setIsKlaar(true);
    });

    // Als hydratatie al klaar is (synchrone MMKV)
    if (useVoorkeurenStore.persist.hasHydrated()) {
      setIsKlaar(true);
    }

    return unsub;
  }, []);

  return isKlaar;
}

Offline-first ondersteuning

Mobiele apps hebben niet de luxe van een stabiele internetverbinding. Gebruikers openen je app in de trein, in parkeergarages, op plekken waar je amper bereik hebt. Iedereen die weleens een app heeft gebouwd kent dat moment: je test alles op kantoor-wifi en vergeet dat de helft van je gebruikers op een 3G-verbinding zit. Met TanStack Query en Zustand kun je daar gelukkig goed mee omgaan:

// providers/OfflineQueryProvider.tsx
import React from 'react';
import { QueryClient } from '@tanstack/react-query';
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
import AsyncStorage from '@react-native-async-storage/async-storage';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      gcTime: 1000 * 60 * 60 * 4, // 4 uur in cache
      staleTime: 1000 * 60 * 5,   // 5 minuten vers
      retry: 2,
    },
  },
});

const asyncStoragePersister = createAsyncStoragePersister({
  storage: AsyncStorage,
  key: 'REACT_QUERY_OFFLINE_CACHE',
  throttleTime: 1000,
});

export function OfflineQueryProvider({ children }: { children: React.ReactNode }) {
  return (
    <PersistQueryClientProvider
      client={queryClient}
      persistOptions={{
        persister: asyncStoragePersister,
        maxAge: 1000 * 60 * 60 * 24, // 24 uur maximale leeftijd
      }}
    >
      {children}
    </PersistQueryClientProvider>
  );
}

Met deze setup ziet de gebruiker bij het openen van de app direct de laatst bekende data. Ondertussen haalt TanStack Query op de achtergrond rustig de nieuwste versie op zodra er weer verbinding is. Dat voelt voor de gebruiker als een app die gewoon altijd werkt — en dat is precies wat je wilt.

Prestatievergelijking: Zustand vs. Redux vs. Context

Waarom Zustand in de meeste gevallen de betere keuze is voor React Native? Laten we de feiten even op een rijtje zetten:

  • Bundlegrootte: Zustand (~1,2 KB) vs. Redux Toolkit (~13,8 KB) vs. Context (0 KB, maar met verborgen kosten). Dat verschil tikt door op mobiel.
  • Re-renders: Zustand met selectors voorkomt onnodige re-renders automatisch. Context triggert een re-render van alle consumers bij elke wijziging — ongeacht welk deel van de state is veranderd. Op een complex scherm merk je dat echt.
  • Boilerplate: Zustand heeft geen Provider, geen action types, geen reducers nodig. Je definieert state en acties op één plek en je bent klaar.
  • MMKV-integratie: Zustand biedt native persist middleware die direct met MMKV werkt — tot 30x sneller dan AsyncStorage-gebaseerde alternatieven.
  • TypeScript: Volledige type-inference zonder extra configuratie. Gewoon werkt het.

Veelgemaakte fouten en hoe je ze vermijdt

Na het zien (en zelf maken) van talloze state management-blunders, zijn dit de fouten die het vaakst voorkomen:

  1. API-data opslaan in Zustand — Dit is veruit de meestgemaakte fout. Gebruik TanStack Query voor alles wat van een server komt. Zustand is er voor client-only state, punt.
  2. Te veel globale state — Een modal die open of dicht staat, een formulierinput, een dropdown-selectie — dat hoort in useState, niet in een globale store. Niet alles hoeft globaal te zijn.
  3. Hele objecten selecteren — Gebruik altijd specifieke selectors (state => state.items) in plaats van de hele store te selecteren. Anders re-rendert je component bij iedere state-wijziging, en dat wil je echt niet.
  4. Alles persisteren — Gebruik partialize om alleen de relevante data op te slaan. Afgeleide waarden of tijdelijke UI-state hoef je niet te persisteren. Sterker nog: het veroorzaakt alleen maar bugs als je dat wel doet.

Veelgestelde vragen

Is Zustand beter dan Redux voor React Native?

Voor de meeste projecten (zeg, teams van 1 tot 10 ontwikkelaars) is Zustand de betere keuze. Minder boilerplate, kleinere bundle, simpelere API. Redux Toolkit heeft nog steeds z'n plek bij grote enterprise-projecten met 10+ ontwikkelaars die baat hebben bij strenge structuur en time-travel debugging. Maar voor de overgrote meerderheid van de apps? Zustand wint.

Kan ik TanStack Query gebruiken zonder Zustand?

Absoluut. TanStack Query vervangt de server state die je voorheen in Redux of Context stopte. Als je app geen complexe client state heeft — geen winkelwagen, geen uitgebreide voorkeuren — kun je prima TanStack Query combineren met gewoon useState en useContext voor de paar stukjes client state die je nodig hebt.

Hoe verhouden de prestaties van MMKV zich tot AsyncStorage?

MMKV is gemiddeld 30x sneller dan AsyncStorage, en dat is geen marketingpraatje. Het komt door het synchrone, native C++-karakter ervan. Met react-native-mmkv v4.3 (een Nitro Module) zijn de prestaties nóg beter geworden. MMKV ondersteunt ook ingebouwde encryptie, waardoor het ideaal is voor gevoelige data zoals authenticatietokens.

Moet ik TanStack Query-cache of Zustand-state offline persisteren?

Beide, maar op verschillende manieren. Gebruik PersistQueryClientProvider met AsyncStorage om de TanStack Query-cache offline beschikbaar te maken — zodat gebruikers direct de laatst bekende data zien bij het openen van de app. Gebruik Zustand's persist middleware met MMKV voor client state die snel beschikbaar moet zijn, zoals gebruikersvoorkeuren en winkelwagen-inhoud.

Werkt Zustand met de Nieuwe Architectuur van React Native?

Ja, zonder problemen. Zustand v5 werkt volledig met de Nieuwe Architectuur (Fabric, JSI, TurboModules). Omdat Zustand een pure JavaScript-library is zonder native dependencies, hoef je niks te migreren. De combinatie met MMKV v4 — die wél de Nieuwe Architectuur vereist — levert de allerbeste prestaties op.

Over de Auteur Editorial Team

Our team of expert writers and editors.