State Management React Native 2026: Panduan Zustand & TanStack Query

Panduan praktis state management di React Native tahun 2026. Pelajari cara menggunakan Zustand untuk client state dan TanStack Query untuk server state, termasuk contoh kode, dukungan offline, dan tips performa.

Pendahuluan: Kenapa Sih State Management Itu Penting Banget?

Kalau kamu sudah cukup lama berkutat dengan React Native, pasti pernah merasakan momen frustasi ketika data di satu screen nggak sinkron dengan screen lain. Atau lebih parahnya, aplikasi jadi lemot karena re-render di mana-mana. Nah, di sinilah state management berperan — dan jujur, ini bisa jadi pembeda antara aplikasi yang nyaman dipakai dan yang bikin pengguna langsung uninstall.

State, atau keadaan, adalah data yang menentukan apa yang ditampilkan di layar pengguna. Mulai dari data user yang sedang login, daftar produk dari API, sampai status toggle dark mode — semuanya adalah state.

Di tahun 2026, ekosistem state management untuk React Native sudah jauh lebih matang dibanding beberapa tahun lalu. Developer berpengalaman sekarang nggak lagi mengandalkan satu library monolitik untuk menangani semua jenis state. Pendekatan yang lebih modern (dan menurut saya, lebih masuk akal) adalah memisahkan state berdasarkan jenisnya: client state untuk data yang hidup sepenuhnya di aplikasi, dan server state untuk data yang berasal dari API atau backend.

Dalam panduan ini, kita akan bahas secara mendalam kombinasi yang sudah jadi semacam standar di industri: Zustand untuk client state dan TanStack Query (dulu dikenal sebagai React Query) untuk server state. Kombinasi ini terbukti bisa mengurangi ukuran bundle hingga 40%, menyederhanakan kode secara drastis, dan memberikan developer experience yang jauh lebih menyenangkan.

Memahami Jenis-Jenis State di React Native

Sebelum langsung loncat ke library, ada baiknya kita pahami dulu berbagai jenis state yang ada di aplikasi React Native. Ini penting, karena setiap jenis state punya karakteristik yang berbeda dan butuh penanganan yang berbeda pula.

Local State (State Lokal)

Local state adalah state yang cuma dibutuhkan oleh satu komponen saja. Contohnya? Nilai input form, status visibility modal, atau toggle sederhana. Untuk hal-hal kayak gini, React bawaan sudah lebih dari cukup:

import { useState } from 'react';
import { View, TextInput, Button, Text } from 'react-native';

function SearchBar() {
  const [query, setQuery] = useState('');
  const [isExpanded, setIsExpanded] = useState(false);

  return (
    <View>
      {isExpanded && (
        <TextInput
          value={query}
          onChangeText={setQuery}
          placeholder="Cari produk..."
        />
      )}
      <Button
        title={isExpanded ? 'Tutup' : 'Cari'}
        onPress={() => setIsExpanded(!isExpanded)}
      />
    </View>
  );
}

Global Client State

Ini adalah data yang perlu diakses oleh banyak komponen di seluruh aplikasi, tapi bukan berasal dari server. Contohnya termasuk tema aplikasi (light/dark mode), preferensi bahasa, status autentikasi, dan state UI global seperti sidebar atau bottom sheet. Nah, di sinilah Zustand bersinar.

Server State

Server state adalah data yang datang dari API eksternal. Data ini bersifat asinkron, bisa berubah tanpa sepengetahuan aplikasi, perlu di-cache untuk performa, dan harus disinkronkan antara klien dan server. Mengelola ini secara manual? Percayalah, itu sangat melelahkan — dan itulah kenapa TanStack Query diciptakan.

Navigation State

Navigation state dikelola oleh library navigasi seperti Expo Router atau React Navigation. Ini mencakup riwayat navigasi, parameter route, dan tab aktif. Umumnya kamu nggak perlu mengelola ini secara manual karena sudah ditangani oleh library navigasi itu sendiri.

Zustand: Solusi Ringan untuk Client State

Zustand (bahasa Jerman untuk "state" atau "keadaan") adalah library state management yang minimalis tapi surprisingly powerful. Dengan ukuran cuma sekitar 1KB setelah minifikasi dan gzip, Zustand menawarkan API berbasis hook yang intuitif tanpa boilerplate berlebihan.

Serius, pertama kali saya coba Zustand, rasanya kayak "kok bisa sesimpel ini?"

Kenapa Zustand di Tahun 2026?

Zustand sudah jadi pilihan utama untuk client state management dengan lebih dari 44.000 bintang di GitHub dan lebih dari 3,5 juta unduhan per minggu. Beberapa keunggulan utamanya:

  • Tidak memerlukan Provider — Berbeda dengan Context API atau Redux, Zustand nggak perlu membungkus aplikasi dengan provider. Ini menghilangkan re-render yang nggak perlu dan menyederhanakan hierarki komponen.
  • Ukuran super kecil — Cuma ~1KB, ideal untuk aplikasi mobile di mana ukuran bundle sangat memengaruhi waktu unduh dan startup.
  • Berbasis subscription — Hanya komponen yang berlangganan pada bagian state tertentu yang akan di-render ulang ketika bagian itu berubah.
  • TypeScript-friendly — Dukungan penuh untuk TypeScript tanpa konfigurasi tambahan.
  • Middleware bawaan — Mendukung persist, devtools, immer, dan lainnya secara out-of-the-box.

Instalasi dan Setup Awal

Memulai dengan Zustand sangat gampang. Instal dulu package-nya:

# Menggunakan npm
npm install zustand

# Menggunakan yarn
yarn add zustand

# Menggunakan Expo
npx expo install zustand

Membuat Store Pertama

Store di Zustand pada dasarnya adalah sebuah hook React yang berisi state dan action. Berikut contoh store autentikasi sederhana yang bisa kamu pakai sebagai starting point:

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

interface User {
  id: string;
  name: string;
  email: string;
  avatarUrl: string;
}

interface AuthState {
  user: User | null;
  isAuthenticated: boolean;
  isLoading: boolean;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
  setUser: (user: User) => void;
}

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

  login: async (email, password) => {
    set({ isLoading: true });
    try {
      const response = await fetch('https://api.example.com/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password }),
      });
      const data = await response.json();
      set({
        user: data.user,
        isAuthenticated: true,
        isLoading: false,
      });
    } catch (error) {
      set({ isLoading: false });
      throw error;
    }
  },

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

  setUser: (user) => set({ user, isAuthenticated: true }),
}));

Menggunakan Store di Komponen

Setelah store-nya jadi, cara pakainya di komponen sangat straightforward — tinggal panggil hook-nya:

// screens/ProfileScreen.tsx
import { View, Text, Image, Pressable, StyleSheet } from 'react-native';
import { useAuthStore } from '../stores/useAuthStore';

export default function ProfileScreen() {
  // Gunakan selector untuk mengambil hanya data yang dibutuhkan
  const user = useAuthStore((state) => state.user);
  const logout = useAuthStore((state) => state.logout);

  if (!user) {
    return (
      <View style={styles.container}>
        <Text style={styles.message}>Silakan login terlebih dahulu</Text>
      </View>
    );
  }

  return (
    <View style={styles.container}>
      <Image source={{ uri: user.avatarUrl }} style={styles.avatar} />
      <Text style={styles.name}>{user.name}</Text>
      <Text style={styles.email}>{user.email}</Text>
      <Pressable style={styles.logoutButton} onPress={logout}>
        <Text style={styles.logoutText}>Keluar</Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, alignItems: 'center', paddingTop: 40 },
  avatar: { width: 100, height: 100, borderRadius: 50 },
  name: { fontSize: 22, fontWeight: 'bold', marginTop: 16 },
  email: { fontSize: 16, color: '#666', marginTop: 4 },
  message: { fontSize: 18, color: '#999' },
  logoutButton: {
    marginTop: 24,
    backgroundColor: '#FF3B30',
    paddingHorizontal: 32,
    paddingVertical: 12,
    borderRadius: 8,
  },
  logoutText: { color: '#fff', fontWeight: '600', fontSize: 16 },
});

Pola Selector: Kunci Performa yang Sering Diabaikan

Ini salah satu hal yang sering dilewatkan developer pemula. Menggunakan selector di Zustand itu bukan opsional — ini kunci performa. Tanpa selector, komponen kamu akan re-render setiap kali ada perubahan apa pun di store, meskipun data yang dipakai nggak berubah.

// BURUK: Berlangganan ke seluruh store
// Komponen akan re-render setiap kali APA PUN di store berubah
const state = useAuthStore();

// BAIK: Berlangganan hanya ke field tertentu
// Komponen hanya re-render ketika 'user' berubah
const user = useAuthStore((state) => state.user);

// BAIK: Mengambil beberapa field sekaligus dengan shallow comparison
import { useShallow } from 'zustand/react/shallow';

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

Pola Slices: Memecah Store yang Sudah Membengkak

Untuk aplikasi yang cukup besar, store bisa jadi terlalu gemuk kalau semuanya dimasukkan ke satu tempat. Solusinya? Pecah jadi beberapa slice yang logis:

// stores/slices/themeSlice.ts
import { StateCreator } from 'zustand';

export interface ThemeSlice {
  isDarkMode: boolean;
  primaryColor: string;
  toggleDarkMode: () => void;
  setPrimaryColor: (color: string) => void;
}

export const createThemeSlice: StateCreator<ThemeSlice> = (set) => ({
  isDarkMode: false,
  primaryColor: '#007AFF',
  toggleDarkMode: () => set((state) => ({ isDarkMode: !state.isDarkMode })),
  setPrimaryColor: (color) => set({ primaryColor: color }),
});

// stores/slices/settingsSlice.ts
export interface SettingsSlice {
  language: string;
  notificationsEnabled: boolean;
  setLanguage: (lang: string) => void;
  toggleNotifications: () => void;
}

export const createSettingsSlice: StateCreator<SettingsSlice> = (set) => ({
  language: 'id',
  notificationsEnabled: true,
  setLanguage: (language) => set({ language }),
  toggleNotifications: () =>
    set((state) => ({ notificationsEnabled: !state.notificationsEnabled })),
});

// stores/useAppStore.ts - Menggabungkan semua slices
import { create } from 'zustand';
import { createThemeSlice, ThemeSlice } from './slices/themeSlice';
import { createSettingsSlice, SettingsSlice } from './slices/settingsSlice';

type AppStore = ThemeSlice & SettingsSlice;

export const useAppStore = create<AppStore>()((...args) => ({
  ...createThemeSlice(...args),
  ...createSettingsSlice(...args),
}));

Persistensi State dengan AsyncStorage

Mau state-nya tetap tersimpan meskipun pengguna menutup dan membuka ulang aplikasi? Pakai middleware persist bareng AsyncStorage:

// stores/useSettingsStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';

interface SettingsState {
  isDarkMode: boolean;
  fontSize: number;
  language: string;
  toggleDarkMode: () => void;
  setFontSize: (size: number) => void;
  setLanguage: (lang: string) => void;
}

export const useSettingsStore = create<SettingsState>()(
  persist(
    (set) => ({
      isDarkMode: false,
      fontSize: 16,
      language: 'id',
      toggleDarkMode: () => set((s) => ({ isDarkMode: !s.isDarkMode })),
      setFontSize: (fontSize) => set({ fontSize }),
      setLanguage: (language) => set({ language }),
    }),
    {
      name: 'settings-storage',
      storage: createJSONStorage(() => AsyncStorage),
      // Opsional: pilih field mana yang akan di-persist
      partialize: (state) => ({
        isDarkMode: state.isDarkMode,
        fontSize: state.fontSize,
        language: state.language,
      }),
    }
  )
);

TanStack Query: Senjata Rahasia untuk Server State

Oke, sekarang kita masuk ke bagian yang menurut saya paling game-changing. TanStack Query (dulu React Query) adalah library yang dirancang khusus untuk mengelola server state. Library ini menangani semua hal rumit seperti caching, sinkronisasi background, pagination, optimistic updates, dan penanganan error — semuanya secara otomatis.

Kalau kamu pernah menulis useEffect + useState + loading + error secara manual untuk fetch data... TanStack Query akan terasa seperti angin segar.

Kenapa TanStack Query?

Mengelola server state secara manual itu jauh lebih kompleks dari yang kebanyakan orang kira. Kamu harus handle loading state, error state, caching, invalidasi cache, refetching, pagination, dan masih banyak lagi. TanStack Query menangani semua ini dengan API yang deklaratif. Beberapa keunggulannya:

  • Caching otomatis — Data yang sudah di-fetch akan di-cache dan dipakai ulang tanpa request tambahan.
  • Background refetching — Data lama ditampilkan dulu sementara data baru di-fetch di background. User nggak perlu lihat loading spinner terus-terusan.
  • Deduplication — Kalau beberapa komponen minta data yang sama, cuma satu request yang dikirim. Efisien banget.
  • Dukungan offline — Dengan konfigurasi yang tepat, aplikasi tetap bisa berfungsi tanpa internet.
  • Window focus refetching — Data otomatis di-refresh saat pengguna kembali ke aplikasi.
  • Retry otomatis — Request yang gagal akan dicoba ulang secara otomatis dengan exponential backoff.

Instalasi dan Konfigurasi

Instal TanStack Query beserta dependency yang diperlukan:

# Instal TanStack Query
npm install @tanstack/react-query

# Opsional: untuk offline persistence
npx expo install @react-native-async-storage/async-storage @react-native-community/netinfo
npm install @tanstack/query-async-storage-persister @tanstack/react-query-persist-client

Setup QueryClient

Konfigurasi QueryClient yang optimal untuk React Native butuh sedikit penyesuaian khusus untuk lingkungan mobile. Berikut konfigurasi yang sudah saya pakai di beberapa proyek dan terbukti bekerja dengan baik:

// lib/queryClient.ts
import { QueryClient } from '@tanstack/react-query';

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // Data dianggap segar selama 5 menit
      staleTime: 1000 * 60 * 5,
      // Cache dibuang setelah 30 menit tidak digunakan
      gcTime: 1000 * 60 * 30,
      // Retry 3 kali dengan exponential backoff
      retry: 3,
      // Nonaktifkan refetch saat window focus (kurang relevan di mobile)
      refetchOnWindowFocus: false,
      // Aktifkan refetch saat koneksi kembali
      refetchOnReconnect: true,
    },
    mutations: {
      retry: 1,
    },
  },
});

// app/_layout.tsx
import { QueryClientProvider } from '@tanstack/react-query';
import { queryClient } from '../lib/queryClient';
import { Stack } from 'expo-router';

export default function RootLayout() {
  return (
    <QueryClientProvider client={queryClient}>
      <Stack />
    </QueryClientProvider>
  );
}

Menggunakan useQuery untuk Fetch Data

Hook useQuery adalah cara utama untuk mengambil data dari server. Coba perhatikan betapa bersihnya kode ini dibanding kalau kamu pakai useEffect manual:

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

interface Product {
  id: string;
  name: string;
  price: number;
  imageUrl: string;
  category: string;
}

async function fetchProducts(category?: string): Promise<Product[]> {
  const url = category
    ? `https://api.example.com/products?category=${category}`
    : 'https://api.example.com/products';
  
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error('Gagal mengambil data produk');
  }
  return response.json();
}

export function useProducts(category?: string) {
  return useQuery({
    queryKey: ['products', { category }],
    queryFn: () => fetchProducts(category),
  });
}

// screens/ProductListScreen.tsx
import { View, Text, FlatList, Image, StyleSheet, ActivityIndicator } from 'react-native';
import { useProducts } from '../hooks/queries/useProducts';

export default function ProductListScreen() {
  const { data: products, isLoading, isError, error, refetch } = useProducts();

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

  if (isError) {
    return (
      <View style={styles.center}>
        <Text style={styles.errorText}>Error: {error.message}</Text>
        <Pressable onPress={() => refetch()} style={styles.retryButton}>
          <Text style={styles.retryText}>Coba Lagi</Text>
        </Pressable>
      </View>
    );
  }

  return (
    <FlatList
      data={products}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => (
        <View style={styles.productCard}>
          <Image source={{ uri: item.imageUrl }} style={styles.productImage} />
          <View style={styles.productInfo}>
            <Text style={styles.productName}>{item.name}</Text>
            <Text style={styles.productPrice}>
              Rp {item.price.toLocaleString('id-ID')}
            </Text>
          </View>
        </View>
      )}
      contentContainerStyle={styles.listContent}
    />
  );
}

Query Key Factory Pattern

Untuk aplikasi yang sudah cukup besar, kamu butuh cara yang konsisten untuk mengelola query key. Pola factory ini mungkin terlihat seperti over-engineering di awal, tapi percayalah — begitu aplikasimu punya belasan endpoint, kamu akan berterima kasih sudah menerapkannya sejak awal:

// lib/queryKeys.ts
export const productKeys = {
  all: ['products'] as const,
  lists: () => [...productKeys.all, 'list'] as const,
  list: (filters: Record<string, unknown>) =>
    [...productKeys.lists(), filters] as const,
  details: () => [...productKeys.all, 'detail'] as const,
  detail: (id: string) => [...productKeys.details(), id] as const,
};

export const userKeys = {
  all: ['users'] as const,
  profile: (id: string) => [...userKeys.all, 'profile', id] as const,
  orders: (userId: string) =>
    [...userKeys.all, 'orders', userId] as const,
};

// Penggunaan
useQuery({
  queryKey: productKeys.detail(productId),
  queryFn: () => fetchProductDetail(productId),
});

// Invalidasi semua data produk
queryClient.invalidateQueries({ queryKey: productKeys.all });

Mutations: Mengubah Data di Server

Hook useMutation digunakan untuk operasi yang mengubah data di server (POST, PUT, DELETE). Yang keren dari TanStack Query adalah dukungan optimistic update-nya. Artinya, UI bisa langsung berubah sebelum server merespons — bikin aplikasi terasa super responsif:

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

interface CartItem {
  productId: string;
  quantity: number;
}

async function addToCart(item: CartItem) {
  const response = await fetch('https://api.example.com/cart', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(item),
  });
  if (!response.ok) throw new Error('Gagal menambahkan ke keranjang');
  return response.json();
}

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

  return useMutation({
    mutationFn: addToCart,
    // Optimistic update: perbarui UI sebelum server merespons
    onMutate: async (newItem) => {
      // Batalkan query yang sedang berjalan agar tidak menimpa optimistic update
      await queryClient.cancelQueries({ queryKey: ['cart'] });

      // Simpan data sebelumnya untuk rollback
      const previousCart = queryClient.getQueryData(['cart']);

      // Update cache secara optimistis
      queryClient.setQueryData(['cart'], (old: CartItem[] | undefined) => {
        return [...(old || []), newItem];
      });

      return { previousCart };
    },
    // Jika gagal, kembalikan ke data sebelumnya
    onError: (_err, _newItem, context) => {
      queryClient.setQueryData(['cart'], context?.previousCart);
    },
    // Setelah sukses atau gagal, selalu refetch data terbaru
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['cart'] });
    },
  });
}

Menggabungkan Zustand dan TanStack Query

Nah, di sinilah keseruan sebenarnya dimulai. Kekuatan terbesar dari kombinasi ini adalah masing-masing library menangani domain yang berbeda tanpa tumpang tindih. Zustand ngurusin client state, TanStack Query ngurusin server state. Bersih, terpisah, dan mudah di-maintain.

Arsitektur yang Direkomendasikan

Berikut struktur folder yang sudah saya pakai dan terbukti scalable untuk proyek React Native berukuran menengah hingga besar:

src/
├── stores/                  # Zustand stores (client state)
│   ├── useAuthStore.ts      # Status autentikasi
│   ├── useThemeStore.ts     # Preferensi tema
│   └── useUIStore.ts        # State UI global
├── hooks/
│   ├── queries/             # TanStack Query hooks (server state)
│   │   ├── useProducts.ts
│   │   ├── useOrders.ts
│   │   └── useUserProfile.ts
│   └── mutations/           # TanStack Query mutations
│       ├── useAddToCart.ts
│       ├── useCreateOrder.ts
│       └── useUpdateProfile.ts
├── lib/
│   ├── queryClient.ts       # Konfigurasi QueryClient
│   └── queryKeys.ts         # Factory pattern untuk query keys
└── api/
    ├── products.ts          # Fungsi fetch untuk produk
    ├── orders.ts            # Fungsi fetch untuk pesanan
    └── auth.ts              # Fungsi fetch untuk autentikasi

Contoh Integrasi: Halaman E-Commerce

Berikut contoh nyata gimana Zustand dan TanStack Query bekerja bersama dalam satu screen. Perhatikan betapa jelasnya pemisahan antara client state dan server state:

// screens/HomeScreen.tsx
import { View, Text, FlatList, Pressable, StyleSheet } from 'react-native';
import { useAuthStore } from '../stores/useAuthStore';
import { useThemeStore } from '../stores/useThemeStore';
import { useProducts } from '../hooks/queries/useProducts';
import { useAddToCart } from '../hooks/mutations/useAddToCart';

export default function HomeScreen() {
  // Client state dari Zustand
  const user = useAuthStore((s) => s.user);
  const isDarkMode = useThemeStore((s) => s.isDarkMode);

  // Server state dari TanStack Query
  const { data: products, isLoading } = useProducts();
  const addToCartMutation = useAddToCart();

  const handleAddToCart = (productId: string) => {
    addToCartMutation.mutate(
      { productId, quantity: 1 },
      {
        onSuccess: () => {
          // Bisa tampilkan notifikasi sukses
        },
        onError: (error) => {
          // Tampilkan pesan error
          console.error('Gagal menambahkan ke keranjang:', error);
        },
      }
    );
  };

  const backgroundColor = isDarkMode ? '#1a1a1a' : '#ffffff';
  const textColor = isDarkMode ? '#ffffff' : '#000000';

  return (
    <View style={[styles.container, { backgroundColor }]}>
      <Text style={[styles.greeting, { color: textColor }]}>
        Halo, {user?.name || 'Pengunjung'}!
      </Text>
      <FlatList
        data={products}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => (
          <Pressable
            style={styles.productCard}
            onPress={() => handleAddToCart(item.id)}
          >
            <Text style={styles.productName}>{item.name}</Text>
            <Text style={styles.productPrice}>
              Rp {item.price.toLocaleString('id-ID')}
            </Text>
          </Pressable>
        )}
      />
    </View>
  );
}

Dukungan Offline dengan TanStack Query

Ini salah satu fitur yang paling relevan untuk aplikasi mobile. Pengguna mobile itu sering banget kehilangan koneksi — di dalam lift, di basement, atau pas lagi di daerah yang sinyalnya jelek. Aplikasi yang tetap bisa berfungsi offline akan memberikan pengalaman yang jauh lebih baik dibanding yang cuma menampilkan layar kosong.

Menyiapkan Offline Persistence

Untuk mengaktifkan persistensi offline, kamu perlu mengkonfigurasi AsyncStorage sebagai persister dan NetInfo untuk mendeteksi status jaringan:

// app/_layout.tsx
import { useEffect } from 'react';
import { Stack } from 'expo-router';
import { onlineManager } 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';
import NetInfo from '@react-native-community/netinfo';
import { queryClient } from '../lib/queryClient';

const asyncStoragePersister = createAsyncStoragePersister({
  storage: AsyncStorage,
  throttleTime: 3000, // Batasi penulisan ke storage
});

export default function RootLayout() {
  useEffect(() => {
    // Sinkronkan status online/offline dengan TanStack Query
    const unsubscribe = NetInfo.addEventListener((state) => {
      const isOnline = !!state.isConnected;
      onlineManager.setOnline(isOnline);
    });
    return () => unsubscribe();
  }, []);

  return (
    <PersistQueryClientProvider
      client={queryClient}
      persistOptions={{ persister: asyncStoragePersister }}
      onSuccess={() => {
        // Saat cache di-restore, jalankan mutasi yang tertunda
        queryClient
          .resumePausedMutations()
          .then(() => queryClient.invalidateQueries());
      }}
    >
      <Stack />
    </PersistQueryClientProvider>
  );
}

Menangani Mutasi Offline

Supaya mutasi tetap tersimpan meskipun aplikasi ditutup dalam kondisi offline, kamu perlu mendefinisikan mutation defaults. Kenapa? Karena fungsi JavaScript nggak bisa diserialisasi — jadi kamu harus memberitahu TanStack Query cara menjalankan ulang mutasi tersebut:

// lib/offlineMutations.ts
import { queryClient } from './queryClient';

export function setupOfflineMutations() {
  // Definisikan mutation defaults agar bisa di-restore setelah app restart
  queryClient.setMutationDefaults(['addToCart'], {
    mutationFn: async (item: { productId: string; quantity: number }) => {
      const response = await fetch('https://api.example.com/cart', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(item),
      });
      return response.json();
    },
  });

  queryClient.setMutationDefaults(['createOrder'], {
    mutationFn: async (order: { items: string[]; address: string }) => {
      const response = await fetch('https://api.example.com/orders', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(order),
      });
      return response.json();
    },
  });
}

Kapan Pakai yang Mana? Panduan Praktis

Oke, ini bagian yang paling sering ditanyakan. Memilih alat yang tepat untuk jenis state yang tepat memang kadang membingungkan, apalagi kalau kamu baru pertama kali menerapkan pola ini. Jadi, ini panduan singkatnya.

Pakai useState / useReducer

Untuk state lokal yang cuma dibutuhkan satu komponen: nilai input form, toggle visibility, state animasi, counter sederhana. Intinya, jangan bikin hal sederhana jadi ribet dengan library eksternal.

Pakai Zustand

Untuk client state global yang perlu diakses banyak komponen: status autentikasi, preferensi tema, state UI kayak modal atau sidebar, data keranjang belanja (kalau cuma di sisi klien), dan preferensi pengguna. Zustand ideal karena ringan, nggak butuh provider, dan gampang banget dipakainya.

Pakai TanStack Query

Untuk semua data yang datang dari server. Titik. Daftar produk, profil pengguna dari API, riwayat pesanan, notifikasi dari server — semuanya pakai TanStack Query. Library ini menangani caching, loading state, error handling, dan sinkronisasi secara otomatis.

Pakai Context API

Untuk data yang jarang berubah dan perlu disediakan ke seluruh pohon komponen: konfigurasi tema, locale/bahasa, dan data yang bersifat quasi-statis. Tapi hati-hati — hindari Context untuk data yang sering berubah karena akan menyebabkan re-render di semua komponen konsumer.

Pertimbangkan Redux Toolkit

Jujur, di 2026 Redux sudah bukan pilihan pertama untuk kebanyakan proyek. Tapi untuk aplikasi enterprise berskala besar dengan tim lebih dari lima developer yang butuh pola ketat dan tooling debugging yang mature, Redux Toolkit masih punya tempat.

Praktik Terbaik dan Tips Performa

Berikut beberapa tips yang sudah saya kumpulkan dari pengalaman mengerjakan beberapa proyek React Native. Semoga berguna.

1. Pisahkan Client State dan Server State

Ini prinsip paling fundamental dan nggak bisa ditawar. Jangan pernah simpan data dari API di Zustand store. Biarkan TanStack Query yang menangani caching, invalidasi, dan sinkronisasi server state. Zustand cuma untuk data yang memang hidup sepenuhnya di klien.

2. Selalu Gunakan Selector di Zustand

Sudah saya sebutkan di atas, tapi ini penting banget untuk diulang. Selector memastikan komponen cuma di-render ulang ketika data yang benar-benar dipakai berubah. Tanpa ini, performa aplikasimu bisa turun drastis seiring bertambahnya state di store.

3. Konfigurasi staleTime yang Tepat

Sesuaikan staleTime di TanStack Query berdasarkan seberapa sering data berubah. Data yang jarang berubah (seperti daftar kategori) bisa punya staleTime lebih lama, sementara data real-time (kayak harga saham) mungkin butuh staleTime yang sangat pendek atau bahkan nol.

4. Hindari Over-fetching

Gunakan query key yang spesifik dan invalidasi cache yang targeted. Jangan ambil ulang semua data ketika cuma satu entitas yang berubah. Pola query key factory yang sudah kita bahas tadi sangat membantu untuk ini.

5. Implementasikan Optimistic Updates

Untuk operasi yang sering dilakukan pengguna (seperti menambahkan ke keranjang atau like postingan), optimistic update bikin UI terasa jauh lebih responsif. Pengguna nggak perlu nunggu respons server untuk melihat hasil aksinya — dan itu membuat pengalaman yang jauh lebih menyenangkan.

6. Manfaatkan Persistensi

Pakai middleware persist di Zustand untuk preferensi pengguna dan AsyncStorage persister di TanStack Query untuk cache offline. Ini memberikan pengalaman yang mulus meskipun pengguna menutup dan membuka ulang aplikasi.

7. Jaga Store Tetap Kecil dan Fokus

Jangan bikin satu "mega store" yang menampung semua state. Pecah jadi beberapa store kecil yang masing-masing punya tanggung jawab jelas. Ini memudahkan testing, debugging, dan maintenance di jangka panjang.

Kesimpulan

State management di React Native sudah berevolusi jauh. Di tahun 2026, pendekatan terbaik bukan lagi memilih satu library untuk semua kebutuhan — melainkan memilih alat yang tepat untuk jenis state yang tepat.

Kombinasi Zustand untuk client state dan TanStack Query untuk server state sudah jadi standar industri karena alasan yang jelas: simpel, performant, dan developer experience-nya luar biasa.

Dengan memahami perbedaan antara client state dan server state, menggunakan selector dengan benar, mengimplementasikan persistensi untuk pengalaman offline, dan mengikuti praktik terbaik yang sudah kita bahas — kamu punya fondasi yang kuat untuk membangun aplikasi React Native yang skalabel dan mudah dipelihara.

Mulailah dari yang sederhana: satu Zustand store untuk autentikasi dan preferensi, plus TanStack Query untuk semua data dari API. Seiring aplikasi berkembang, tinggal tambahkan store dan query baru tanpa harus mengubah arsitektur dasar. Itulah keindahan dari pendekatan modular ini — ia tumbuh bersama aplikasimu.

Tentang Penulis Editorial Team

Our team of expert writers and editors.