Upravljanje stanjem u React Native 2026: Zustand, TanStack Query i MMKV u praksi

Naučite kako organizirati stanje u React Native aplikaciji koristeći Zustand za klijentsko stanje, TanStack Query za serverske podatke i MMKV za brzu persistenciju. Praktični vodič s primjerima koda.

Zašto je upravljanje stanjem u React Nativeu toliko bitno

Ako ste ikada radili na React Native aplikaciji koja je prerasla fazu prototipa, znate koliko brzo stvari postanu kaotične. Prop drilling kroz pet razina komponenata, duplirani API pozivi na različitim ekranima, stanje koje se misteriozno resetira kad korisnik navigira unatrag — sve to zvuči poznato, zar ne? To su klasični simptomi lošeg upravljanja stanjem, i iskreno, svi smo kroz to prošli barem jednom.

U 2026., ekosustav je konačno dovoljno sazrio da imamo jasne odgovore na vječno pitanje: kako pravilno organizirati stanje u mobilnoj aplikaciji?

Odgovor više nije jedan alat za sve. Moderni pristup razdvaja stanje u dvije kategorije: klijentsko stanje (UI state, korisničke preferencije, stanje formi) i serversko stanje (podaci s API-ja, kesiranje, sinkronizacija). Za svaku kategoriju koristi se specijalizirani alat — i upravo to ćemo detaljno razraditi ovdje.

Moderna arhitektura stanja: tri sloja

Ajmo prvo razumjeti arhitekturu koju koristi većina uspješnih React Native aplikacija danas. Radi se o tri jasno odvojena sloja:

  • Zustand — za klijentsko stanje (tema, autentifikacija, UI toggleovi, filtri)
  • TanStack Query (React Query) — za serversko stanje (API podaci, kesiranje, revalidacija)
  • MMKV — za trajno spremanje podataka na uređaj (persistencija stanja između pokretanja aplikacije)

Ova kombinacija je postala de facto standard. Prema podacima iz 2026., Zustand se koristi u otprilike 40% novih React projekata, dok TanStack Query pokriva oko 80% obrazaca serverskog stanja. Zajedno čine robustan sustav koji radi jednako dobro u maloj aplikaciji kao i u nečem enterprise-razine.

Zustand v5: klijentsko stanje bez boilerplatea

Što je Zustand i zašto ga koristiti

Zustand (njemački za "stanje") je minimalistička biblioteka za upravljanje stanjem koja koristi hookove i jednostavan API. Za razliku od Reduxa, ne zahtijeva providere, reducere ni akcijske tipove. Cijela biblioteka je velika samo oko 2 KB (minimizirano i gzipano) — to je doslovno desetak puta manje od Redux Toolkita.

Zustand v5, trenutna stabilna verzija (5.0.11), zahtijeva React 18+ i koristi nativni useSyncExternalStore umjesto polyfilla. U praksi to znači manje ovisnosti i bolju kompatibilnost s React Native novom arhitekturom.

Kreiranje prvog storea

Evo kako izgleda tipičan Zustand store za autentifikaciju u React Native aplikaciji:

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

interface AuthState {
  user: { id: string; name: string; email: string } | null;
  token: string | null;
  isAuthenticated: boolean;
  login: (user: AuthState['user'], token: string) => void;
  logout: () => void;
  updateUser: (updates: Partial<NonNullable<AuthState['user']>>) => void;
}

export const useAuthStore = create<AuthState>((set) => ({
  user: null,
  token: null,
  isAuthenticated: false,

  login: (user, token) =>
    set({ user, token, isAuthenticated: true }),

  logout: () =>
    set({ user: null, token: null, isAuthenticated: false }),

  updateUser: (updates) =>
    set((state) => ({
      user: state.user ? { ...state.user, ...updates } : null,
    })),
}));

Primijetite koliko je ovo jednostavno. Nema akcijskih tipova, nema switch/case blokova, nema dispatchera. Stanje i funkcije koje ga mijenjaju definirani su na jednom mjestu, i to je to. Provideri? Nisu potrebni — store je globalno dostupan kroz hook.

Korištenje u komponentama

// screens/ProfileScreen.tsx
import React from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import { useAuthStore } from '../stores/useAuthStore';

export function ProfileScreen() {
  // Selektori — komponenta se rerenderira SAMO kad se ove
  // vrijednosti promijene, ne kod svake promjene storea
  const user = useAuthStore((state) => state.user);
  const logout = useAuthStore((state) => state.logout);

  if (!user) return null;

  return (
    <View>
      <Text>{user.name}</Text>
      <Text>{user.email}</Text>
      <TouchableOpacity onPress={logout}>
        <Text>Odjava</Text>
      </TouchableOpacity>
    </View>
  );
}

Ključna stvar ovdje su selektori. Umjesto da povučete cijeli store (useAuthStore()), uvijek selektirajte samo ono što vam treba. Ovo sprječava nepotrebne rerendere i može značajno poboljšati performanse — pogotovo na slabijim uređajima, gdje svaki nepotreban render osjetno usporava UI.

Organizacija storeova u većoj aplikaciji

Za veće aplikacije, preporučujem organizaciju po domeni:

stores/
├── useAuthStore.ts        // autentifikacija
├── useSettingsStore.ts    // korisničke postavke (tema, jezik)
├── useCartStore.ts        // košarica (e-commerce)
└── useUIStore.ts          // UI stanje (modali, draweri, filtri)

Svaki store je neovisan i fokusiran na jednu domenu. Po mom iskustvu, ovo je puno bolje od jednog ogromnog globalnog storea, iz nekoliko razloga:

  • Lakše se testira
  • Komponente se pretplaćuju samo na relevantne promjene
  • Tim može raditi na različitim storeovima paralelno bez konflikata

TanStack Query: serversko stanje pod kontrolom

Problem koji TanStack Query rješava

Evo situacije koju ste gotovo sigurno doživjeli: imate listu proizvoda na jednom ekranu, detalje proizvoda na drugom, i košaricu na trećem. Korisnik promijeni nešto na jednom ekranu, navigira na drugi — i vidi zastarjele podatke. Ili još gore, imate isti API poziv na tri mjesta u aplikaciji, a sva tri se izvršavaju nezavisno. Frustrirajuće, zar ne?

TanStack Query (prije poznat kao React Query) rješava upravo te probleme. On upravlja kompletnim životnim ciklusom serverskih podataka: dohvaćanje, kesiranje, revalidacija, ponovni pokušaji, i optimistička ažuriranja. Jednom kad ga počnete koristiti, teško je zamisliti život bez njega.

Konfiguracija za React Native

// App.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // Podaci su "svježi" 5 minuta — nema refetcha u tom periodu
      staleTime: 5 * 60 * 1000,
      // Keš se čisti nakon 30 minuta neaktivnosti
      gcTime: 30 * 60 * 1000,
      // 3 pokušaja s eksponencijalnim backoffom
      retry: 3,
      retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
      // Manje relevantno za mobilne aplikacije
      refetchOnWindowFocus: false,
      // Bitno za mobilne uređaje — refetch kad se vrati konekcija
      refetchOnReconnect: true,
    },
  },
});

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      {/* ostatak aplikacije */}
    </QueryClientProvider>
  );
}

Jedna napomena: refetchOnWindowFocus: false je skoro uvijek dobra ideja za mobilne aplikacije. Na webu ta opcija ima smisla, ali u React Nativeu samo stvara nepotrebne API pozive.

Query Key Factory obrazac

Organizacija query ključeva postaje kritična čim aplikacija malo naraste. Obrazac koji se pokazao najboljim je tvornica ključeva (key factory):

// api/queryKeys.ts
export const queryKeys = {
  products: {
    all: ['products'] as const,
    list: (filters: ProductFilters) =>
      ['products', 'list', filters] as const,
    detail: (id: string) =>
      ['products', 'detail', id] as const,
  },
  users: {
    all: ['users'] as const,
    profile: (id: string) =>
      ['users', 'profile', id] as const,
  },
  orders: {
    all: ['orders'] as const,
    byUser: (userId: string) =>
      ['orders', 'user', userId] as const,
    detail: (orderId: string) =>
      ['orders', 'detail', orderId] as const,
  },
};

Ovaj obrazac omogućuje lako invalidiranje keša na različitim razinama granularnosti. Recimo, nakon kreiranja narudžbe možete invalidirati sve narudžbe korisnika jednim pozivom:

queryClient.invalidateQueries({
  queryKey: queryKeys.orders.byUser(userId)
});

Custom hook za dohvaćanje podataka

// hooks/useProducts.ts
import { useQuery } from '@tanstack/react-query';
import { queryKeys } from '../api/queryKeys';
import { api } from '../api/client';

interface ProductFilters {
  category?: string;
  search?: string;
  page?: number;
}

export function useProducts(filters: ProductFilters = {}) {
  return useQuery({
    queryKey: queryKeys.products.list(filters),
    queryFn: () => api.get('/products', { params: filters }),
    // Zadrži prethodne podatke dok se novi učitavaju
    placeholderData: (previousData) => previousData,
  });
}

Mutacije s optimističkim ažuriranjima

Za operacije pisanja (POST, PUT, DELETE), TanStack Query nudi mutacije s podrškom za optimistička ažuriranja. Ideja je jednostavna — korisnik odmah vidi promjenu u UI-ju, a ako API poziv ne uspije, stanje se automatski vraća na prethodno. Korisničko iskustvo je dramatično bolje.

// hooks/useToggleFavorite.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { queryKeys } from '../api/queryKeys';
import { api } from '../api/client';

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

  return useMutation({
    mutationFn: (productId: string) =>
      api.post(`/products/${productId}/favorite`),

    // Optimističko ažuriranje — odmah ažuriraj UI
    onMutate: async (productId) => {
      // Otkaži tekuće upite da ne prepišu optimistički update
      await queryClient.cancelQueries({
        queryKey: queryKeys.products.detail(productId),
      });

      // Spremi prethodno stanje za rollback
      const previous = queryClient.getQueryData(
        queryKeys.products.detail(productId)
      );

      // Optimistički ažuriraj keš
      queryClient.setQueryData(
        queryKeys.products.detail(productId),
        (old: any) => ({
          ...old,
          isFavorite: !old.isFavorite,
        })
      );

      return { previous };
    },

    // Ako API poziv ne uspije, vrati prethodno stanje
    onError: (_err, productId, context) => {
      if (context?.previous) {
        queryClient.setQueryData(
          queryKeys.products.detail(productId),
          context.previous
        );
      }
    },

    // Uvijek revalidiraj nakon mutacije
    onSettled: (_data, _err, productId) => {
      queryClient.invalidateQueries({
        queryKey: queryKeys.products.detail(productId),
      });
    },
  });
}

Da, koda je dosta — ali jednom kad ovo postavite, imate bullet-proof sustav za upravljanje favoritima s automatskim rollbackom. Isplati se.

Zustand + TanStack Query: spajanje klijentskog i serverskog stanja

Zlatno pravilo

Jedno kritično pravilo koje ću odmah naglasiti: nikada ne spremajte serverske podatke u Zustand. TanStack Query upravlja kešom, revalidacijom i sinkronizacijom — dupliciranje tih podataka u Zustand store stvara bugove sinkronizacije koje je izuzetno teško dijagnosticirati. Vjerujte mi na riječ, proći ćete kroz sat ili dva čudnog debuggiranja prije nego shvatite što se događa.

Praktični primjer: filter + dohvaćanje

Evo elegantnog obrasca koji zapravo pokazuje pravu snagu ove kombinacije. Zustand upravlja filterima (klijentsko stanje), a TanStack Query koristi te filtere za dohvaćanje podataka (serversko stanje):

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

interface FilterState {
  category: string | null;
  search: string;
  sortBy: 'price' | 'name' | 'rating';
  setCategory: (category: string | null) => void;
  setSearch: (search: string) => void;
  setSortBy: (sortBy: FilterState['sortBy']) => void;
  resetFilters: () => void;
}

export const useFilterStore = create<FilterState>((set) => ({
  category: null,
  search: '',
  sortBy: 'name',
  setCategory: (category) => set({ category }),
  setSearch: (search) => set({ search }),
  setSortBy: (sortBy) => set({ sortBy }),
  resetFilters: () =>
    set({ category: null, search: '', sortBy: 'name' }),
}));
// screens/ProductListScreen.tsx
import React from 'react';
import { FlatList, View, Text, ActivityIndicator } from 'react-native';
import { useFilterStore } from '../stores/useFilterStore';
import { useProducts } from '../hooks/useProducts';

export function ProductListScreen() {
  // Klijentsko stanje iz Zustand
  const category = useFilterStore((s) => s.category);
  const search = useFilterStore((s) => s.search);
  const sortBy = useFilterStore((s) => s.sortBy);

  // Serversko stanje iz TanStack Query
  // Automatski se refetcha kad se filtri promijene!
  const { data, isLoading, error } = useProducts({
    category: category ?? undefined,
    search,
    sortBy,
  });

  if (isLoading) return <ActivityIndicator />;
  if (error) return <Text>Greška: {error.message}</Text>;

  return (
    <FlatList
      data={data}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => (
        <View>
          <Text>{item.name}</Text>
          <Text>{item.price} EUR</Text>
        </View>
      )}
    />
  );
}

Ovo je moćan obrazac jer je potpuno reaktivan. Kad korisnik promijeni filter u Zustand storeu, TanStack Query automatski pokreće novi upit jer se query key promijenio. Nema ručnog upravljanja loadingom, nema dupliciranih poziva, a prethodni podaci ostaju vidljivi dok se novi učitavaju. Elegantno, zar ne?

MMKV: brza persistencija na uređaju

Zašto MMKV umjesto AsyncStorage

AsyncStorage je dugo bio standardni izbor za trajno spremanje podataka u React Nativeu, ali ima ozbiljne nedostatke. Asinkron je (dakle, await za svaku operaciju), relativno spor, i nema podršku za enkripciju. MMKV, originalno razvijen od strane WeChata za njihovu masivnu mobilnu aplikaciju, rješava sve te probleme:

  • Do 30x brži od AsyncStoragea
  • Sinkrone operacije — nema awaita, nema Promisea
  • Ugrađena enkripcija — idealno za tokene i osjetljive podatke
  • Multi-process podrška — sigurno čitanje/pisanje iz više procesa
  • Mala veličina — minimalan utjecaj na veličinu aplikacije

Iskreno, nakon što sam prešao na MMKV, AsyncStorage mi djeluje kao nešto iz kamenog doba.

Instalacija

# Instalacija
npx expo install react-native-mmkv

# Za bare React Native projekte
npm install react-native-mmkv
cd ios && pod install

Zustand + MMKV persist middleware

Evo kako spojiti Zustand s MMKV-om za automatsku persistenciju stanja. Ovo je jedan od onih setupova koji, kad ga jednom napravite, zaboravite da postoji — jednostavno radi.

// lib/storage.ts
import { MMKV } from 'react-native-mmkv';
import { StateStorage } from 'zustand/middleware';

// Kreiranje MMKV instance
const storage = new MMKV({
  id: 'app-storage',
});

// Adapter za Zustand persist middleware
export const mmkvStorage: StateStorage = {
  setItem: (name, value) => {
    storage.set(name, value);
  },
  getItem: (name) => {
    return storage.getString(name) ?? null;
  },
  removeItem: (name) => {
    storage.delete(name);
  },
};

// Enkriptirana instanca za osjetljive podatke
const secureStorage = new MMKV({
  id: 'secure-storage',
  encryptionKey: 'your-encryption-key',
});

export const secureMmkvStorage: StateStorage = {
  setItem: (name, value) => {
    secureStorage.set(name, value);
  },
  getItem: (name) => {
    return secureStorage.getString(name) ?? null;
  },
  removeItem: (name) => {
    secureStorage.delete(name);
  },
};
// stores/useSettingsStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { mmkvStorage } from '../lib/storage';

interface SettingsState {
  theme: 'light' | 'dark' | 'system';
  language: string;
  notificationsEnabled: boolean;
  setTheme: (theme: SettingsState['theme']) => void;
  setLanguage: (language: string) => void;
  toggleNotifications: () => void;
}

export const useSettingsStore = create<SettingsState>()(
  persist(
    (set) => ({
      theme: 'system',
      language: 'hr',
      notificationsEnabled: true,
      setTheme: (theme) => set({ theme }),
      setLanguage: (language) => set({ language }),
      toggleNotifications: () =>
        set((state) => ({
          notificationsEnabled: !state.notificationsEnabled,
        })),
    }),
    {
      name: 'settings-storage',
      storage: createJSONStorage(() => mmkvStorage),
    }
  )
);

Persistirani auth store s enkriptiranim MMKV-om

Za auth podatke (tokene, korisničke podatke), definitivno želite koristiti enkriptiranu MMKV instancu. Evo kako to izgleda u praksi:

// stores/useAuthStore.ts (s persistencijom)
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { secureMmkvStorage } from '../lib/storage';

interface AuthState {
  user: { id: string; name: string; email: string } | null;
  token: string | null;
  isAuthenticated: boolean;
  login: (user: AuthState['user'], token: string) => void;
  logout: () => void;
}

export const useAuthStore = create<AuthState>()(
  persist(
    (set) => ({
      user: null,
      token: null,
      isAuthenticated: false,
      login: (user, token) =>
        set({ user, token, isAuthenticated: true }),
      logout: () =>
        set({ user: null, token: null, isAuthenticated: false }),
    }),
    {
      name: 'auth-storage',
      storage: createJSONStorage(() => secureMmkvStorage),
      // Spremaj samo ono što je potrebno za persistenciju
      partialize: (state) => ({
        user: state.user,
        token: state.token,
        isAuthenticated: state.isAuthenticated,
      }),
    }
  )
);

Obratite pažnju na partialize — ovo je bitno. Njime kontrolirate koje dijelove stanja želite persistirati. Funkcije (poput login i logout) se nikad ne trebaju spremati jer se rekonstruiraju pri kreiranju storea.

Upravljanje hidratacijom

Ovo je detalj koji mnogi preskoče, a može stvoriti neugodan bug. Kad koristite persistirano stanje, postoji kratki trenutak tijekom pokretanja aplikacije kada store još sadrži inicijalne vrijednosti — takozvana hidratacija. Bez pravilnog upravljanja, korisnik može vidjeti login ekran na pola sekunde prije nego se učita spremljeni token. Ne izgleda profesionalno.

// components/HydrationGate.tsx
import React from 'react';
import { ActivityIndicator, View } from 'react-native';
import { useAuthStore } from '../stores/useAuthStore';
import { useSettingsStore } from '../stores/useSettingsStore';

interface Props {
  children: React.ReactNode;
}

export function HydrationGate({ children }: Props) {
  const [isReady, setIsReady] = React.useState(false);

  React.useEffect(() => {
    const unsub1 = useAuthStore.persist.onFinishHydration(() => {
      checkReady();
    });
    const unsub2 = useSettingsStore.persist.onFinishHydration(() => {
      checkReady();
    });

    function checkReady() {
      if (
        useAuthStore.persist.hasHydrated() &&
        useSettingsStore.persist.hasHydrated()
      ) {
        setIsReady(true);
      }
    }

    // Možda su već hidratizirani (MMKV je sinkron, pa je ovo često slučaj)
    checkReady();

    return () => {
      unsub1();
      unsub2();
    };
  }, []);

  if (!isReady) {
    return (
      <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
        <ActivityIndicator size="large" />
      </View>
    );
  }

  return <>{children}</>;
}

S MMKV-om, hidratacija je gotovo trenutna (jer su operacije sinkrone), ali ovaj gate je i dalje dobra praksa — nikad ne znate kad će se nešto neočekivano usporiti.

Usporedba biblioteka za upravljanje stanjem

Da biste lakše odlučili koji alat koristiti u kojem scenariju, evo detaljne usporedbe najpopularnijih opcija:

KriterijZustandRedux ToolkitJotaiContext API
Veličina paketa~2 KB~15 KB~4 KBUgrađeno
BoilerplateMinimalanSrednjiMinimalanNizak
ProvideriNe trebaObaveznoObaveznoObavezno
Granularnost rerenderaVisoka (selektori)SrednjaVrlo visoka (atomi)Niska
DevToolsOsnovnoOdličnoOgraničenoReact DevTools
Krivulja učenjaNiskaSrednja-visokaNiskaVrlo niska
TypeScriptOdličanDobarOdličanDobar
Middlewarepersist, devtools, immerUgrađenAtomi s efektimaNema
Idealno zaMale-velike aplikacijeEnterpriseSloženo međuzavisno stanjeJednostavno dijeljeno stanje

Osobno, za nove React Native projekte gotovo uvijek počinjem sa Zustandom. Ako se pokaže da mi treba atomski pristup, Jotai je odlična alternativa (i dolazi od istog tima).

Praktični savjeti i česte zamke

1. Ne stavljajte sve u globalno stanje

Ovo je daleko najčešća greška koju vidim. Ako se neko stanje koristi samo unutar jedne komponente — recimo, je li modal otvoren ili vrijednost nekog input polja — koristite lokalni useState. Globalni store je za stanje koje dijeli više komponenata ili koje mora preživjeti navigaciju između ekrana.

2. Uvijek koristite selektore u Zustand v5

U Zustand v5 postoji jedna bitna promjena ponašanja koja zna uloviti ljude nespremne. Ako selektor vrati novu referencu (novi objekt), to može uzrokovati beskonačne petlje rerendera. Evo kako to izbjeći:

// KRIVO — kreira novi objekt svaki put
const { user, token } = useAuthStore((state) => ({
  user: state.user,
  token: state.token,
}));

// ISPRAVNO — koristite shallow za usporedbu
import { useShallow } from 'zustand/react/shallow';

const { user, token } = useAuthStore(
  useShallow((state) => ({
    user: state.user,
    token: state.token,
  }))
);

// ILI JOŠ BOLJE — odvojeni selektori
const user = useAuthStore((state) => state.user);
const token = useAuthStore((state) => state.token);

Osobno preferiram odvojene selektore. Čitljiviji su i ne morate razmišljati o shallow usporedbi.

3. Zustand v5 i defaultna persistencija

Mala ali važna promjena: u Zustand v5, defaultno stanje se više ne sprema automatski u storage ako nikada nije bilo promijenjeno. Znači, ako korisnik prvi put pokrene aplikaciju i ne promijeni nijednu postavku, te defaultne vrijednosti neće biti persisitirane. Ako vam je to bitno (a ponekad jest), eksplicitno pokrenite spremanje nakon inicijalizacije.

4. Koristite TanStack Query za sve API pozive

Nemojte ručno dohvaćati podatke s useEffect + fetch i spremati ih u Zustand. Znam da je primamljivo jer djeluje jednostavnije, ali TanStack Query automatski upravlja kešom, loading stanjima, greškama, ponovnim pokušajima i revalidacijom. Ručno rješenje nikad neće postići istu razinu robusnosti bez značajnog dodatnog koda.

5. Razdvojite MMKV instance po domeni

Umjesto jedne globalne MMKV instance, kreirajte odvojene instance za različite domene podataka. Ovo olakšava selektivno čišćenje — na primjer, pri odjavi brišete auth storage, ali zadržavate korisničke postavke. Također omogućuje različite razine enkripcije za različite tipove podataka.

Postavljanje kompletnog projekta

Ako krećete od nule, evo korak-po-korak uputa za postavljanje svega u novom Expo projektu:

# 1. Kreirajte novi Expo projekt
npx create-expo-app@latest MojaAplikacija
cd MojaAplikacija

# 2. Instalirajte potrebne pakete
npx expo install react-native-mmkv
npm install zustand @tanstack/react-query

# 3. Struktura projekta
mkdir -p src/stores src/hooks src/api src/lib src/components
// src/lib/storage.ts — MMKV adapter (kao gore)
// src/stores/useAuthStore.ts — Auth store s persist
// src/stores/useSettingsStore.ts — Settings store s persist
// src/api/queryKeys.ts — Query key factory
// src/api/client.ts — API klijent
// src/components/HydrationGate.tsx — Gate za hidrataciju

// App.tsx — sve spojeno
import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { HydrationGate } from './src/components/HydrationGate';
import { RootNavigator } from './src/navigation/RootNavigator';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000,
      gcTime: 30 * 60 * 1000,
      refetchOnWindowFocus: false,
      refetchOnReconnect: true,
    },
  },
});

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <HydrationGate>
        <RootNavigator />
      </HydrationGate>
    </QueryClientProvider>
  );
}

I to je to — tri paketa, čista arhitektura, i sustav koji može rasti s vašom aplikacijom bez da postane neodrživ.

Često postavljana pitanja (FAQ)

Trebam li Zustand ako već koristim TanStack Query?

Da, ali za različite stvari. TanStack Query upravlja serverskim podacima (API odgovori, kesiranje, sinkronizacija), dok Zustand upravlja klijentskim stanjem (UI preferencije, stanje formi, filtri, tema). Ova dva alata se savršeno nadopunjuju i ne bi trebali duplicirati istu funkcionalnost. Ako vaša aplikacija ima zaista minimalno klijentsko stanje, možete koristiti samo TanStack Query s React Context za preostalo — ali to je rijedak slučaj u praksi.

Je li Zustand bolji od Reduxa za React Native?

Za većinu React Native aplikacija u 2026. — da, po mom mišljenju jest. Zustand nudi značajno manje boilerplatea, 10x manju veličinu paketa, bolju integraciju s React Native novom arhitekturom, i ne zahtijeva providere. Redux Toolkit i dalje ima smisla za velike enterprise timove koji trebaju stroge obrasce, opsežne DevTools i bogat middleware ekosustav. Ali za timove od 1 do 10 developera, Zustand je gotovo uvijek bolji izbor.

Kako migrirati s AsyncStorage na MMKV?

Migracija je relativno jednostavna: instalirajte react-native-mmkv, kreirajte MMKV adapter za Zustand persist middleware (kao što je prikazano u ovom vodiču), i zamijenite createJSONStorage(() => AsyncStorage) s createJSONStorage(() => mmkvStorage). Jedna stvar koju treba imati na umu: korisnici će izgubiti prethodno persisitirano stanje jer se storage backend promijenio. Zato implementirajte graceful fallback za prvo pokretanje nakon ažuriranja.

Mogu li koristiti Jotai umjesto Zustand?

Apsolutno. Jotai je odličan izbor, posebno za aplikacije s kompleksnim međuzavisnim stanjem — recimo, kad promjena jedne vrijednosti treba automatski ažurirati više drugih izvedenih vrijednosti. Zustand koristi pristup "odozgo prema dolje" s jednim storeom, dok Jotai koristi atomski pristup "odozdo prema gore". Oba su kreirana od istog tima (pmndrs) i imaju sličnu veličinu paketa. Za većinu mobilnih aplikacija, izbor je stvar preferencije.

Kako upravljati offline stanjem u React Native?

Kombinacija TanStack Query + MMKV pokriva većinu offline scenarija i to prilično elegantno. TanStack Query ima ugrađenu podršku za offline persistenciju keša putem persistQueryClient plugina. U kombinaciji s MMKV-om kao storage backendom, vaša aplikacija može prikazati kešane podatke čak i bez internetske veze. Za složenije offline scenarije (optimističke mutacije, red čekanja za API pozive), pogledajte TanStack Query-jev onlineManager i focusManager adaptirane za React Native.

O Autoru Editorial Team

Our team of expert writers and editors.