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.