Penyimpanan lokal adalah tempat aplikasi menaruh data di HP supaya data itu masih ada setelah aplikasinya ditutup. Di pelajaran state kamu lihat sendiri: filter "Disetrika" hilang begitu sistem mematikan aplikasi di background. Bayangin meja kasir Bu Rini. Ada buku catatan di laci (harus dibuka dulu, sebentar, baru terbaca), papan tulis kecil di dinding (sekali lirik langsung terbaca), brankas untuk kunci toko, dan lemari arsip berlaci untuk ribuan nota. Di HP, keempatnya adalah AsyncStorage, MMKV, SecureStore, dan SQLite.
🧪 Tebakan cepat: Filter "Disetrika" disimpan dengan Zustand
persistke AsyncStorage. Mbak Wulan menutup aplikasi lalu membukanya lagi. Di render pertama, chip filter mana yang aktif? Jawabannya ada di bagian "Menyimpan filter: AsyncStorage dan MMKV".
Empat tempat menyimpan di HP
Versi yang dipasang npx expo install di Expo SDK 57 (dicek 11 Oktober 2026):
| Penyimpanan | Versi | Cara baca | Expo Go | Cocok untuk |
|---|---|---|---|---|
@react-native-async-storage/async-storage |
2.2.0 | async: await getItem(key) |
ada | pengaturan kecil, cache kecil |
react-native-mmkv + react-native-nitro-modules |
4.3.2 + 0.37.1 | sync: getString(key) |
gak ada | pengaturan yang dibaca saat start |
expo-secure-store |
57.0.4 | async, terenkripsi | ada | token login, rahasia kecil |
expo-sqlite |
57.0.4 | query SQL | ada | data banyak yang dicari dan disaring |
- AsyncStorage menyimpan pasangan kunci dan nilai, nilainya string, tanpa enkripsi. Setiap panggilan mengembalikan Promise. Di Android totalnya dibatasi 6 MB secara bawaan. Di npm sudah ada versi 3.1.1 (dengan
createAsyncStoragedangetMany/setMany), tapi SDK 57 memasang 2.2.0, versi yang diuji Expo. - MMKV (buatan WeChat) membaca dan menulis secara synchronous lewat JSI, antarmuka langsung JavaScript ke C++. Versi 4 dibuat dengan
createMMKV(), bukannew MMKV()lagi, dan.delete()jadi.remove(). README-nya mengklaim sekitar 30 kali lebih cepat dari AsyncStorage (membaca satu nilai 1.000 kali). Karena kode native-nya gak ada di Expo Go, kamu butuh development build. Di Expo Go dia berhenti denganNitroModules are not supported in Expo Go!. - SecureStore memakai Keychain di iOS, dan SharedPreferences yang dienkripsi dengan Android Keystore. Dokumentasinya mengingatkan: nilai besar bisa ditolak (beberapa versi iOS menolak di atas sekitar 2.048 byte). Jadi simpan token, bukan data order.
- expo-sqlite adalah database sungguhan. Bonusnya,
expo-sqlite/kv-storepunya API yang sama dengan AsyncStorage, plus versi sync sepertigetItemSync.
Menyimpan filter: AsyncStorage dan MMKV
// src/state/filter.ts
import AsyncStorage from '@react-native-async-storage/async-storage';
import { create } from 'zustand';
import { createJSONStorage, persist } from 'zustand/middleware';
import type { Status } from '@/data/orders';
type Filter = 'Semua' | Status;
export const useFilter = create<{ filter: Filter; setFilter: (filter: Filter) => void }>()(
persist(
(set) => ({ filter: 'Semua', setFilter: (filter) => set({ filter }) }),
{ name: 'filter-pesanan', storage: createJSONStorage(() => AsyncStorage) },
),
);
Setiap setFilter('Disetrika') menulis kunci filter-pesanan berisi {"state":{"filter":"Disetrika"},"version":0}. Fungsi gak ikut tersimpan, karena JSON.stringify melewatinya. Versi MMKV cuma mengganti storage:
import { createMMKV } from 'react-native-mmkv';
import type { StateStorage } from 'zustand/middleware';
const mmkv = createMMKV();
const penyimpanan: StateStorage = {
getItem: (key) => mmkv.getString(key) ?? null,
setItem: (key, value) => mmkv.set(key, value),
removeItem: (key) => { mmkv.remove(key); },
};
// ...lalu storage: createJSONStorage(() => penyimpanan)
Yang terjadi saat aplikasi dibuka lagi:
| Saat | AsyncStorage | MMKV |
|---|---|---|
| store dibuat | getItem mengembalikan Promise, belum selesai |
getString langsung mengembalikan JSON-nya |
| render pertama | filter 'Semua': chip Semua menyala, daftar belum disaring |
filter 'Disetrika': chip Disetrika sudah menyala |
| Promise selesai | filter jadi 'Disetrika', render ulang |
gak ada perubahan |
Jadi dengan AsyncStorage, render pertama masih 'Semua', lalu sekejap kemudian berganti ke "Disetrika": daftarnya berkedip. Itu jawaban tebakannya. Zustand cuma bisa memulihkan state secara synchronous kalau penyimpanannya synchronous. Pilihannya: MMKV, atau tahan splash screen sampai useFilter.persist.hasHydrated() bernilai true.
Daftar order untuk mulai cepat: persist TanStack Query
Daftar order adalah server state, jadi yang disimpan adalah cache TanStack Query, lewat @tanstack/react-query-persist-client dan @tanstack/query-async-storage-persister (5.104.1):
// src/app/_layout.tsx (bagian penyedia query)
import AsyncStorage from '@react-native-async-storage/async-storage';
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
import { QueryClient } from '@tanstack/react-query';
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
const queryClient = new QueryClient({
defaultOptions: { queries: { gcTime: 1000 * 60 * 60 * 24 } }, // 24 jam, sama dengan maxAge
});
const persister = createAsyncStoragePersister({ storage: AsyncStorage });
// di RootLayout: bungkus <Stack /> dengan
<PersistQueryClientProvider client={queryClient} persistOptions={{ persister, maxAge: 1000 * 60 * 60 * 24 }}>
- Persister menulis seluruh cache ke kunci
REACT_QUERY_OFFLINE_CACHE, paling sering sekali per detik. - Saat dibuka, cache dipulihkan dulu (selama itu
useQuerymenunggu), lalu daftar['orders','saya']tampil dari cache tanpa menunggu server: #13 "Disetrika". Setelah itu query mengambil ulang dan #13 berubah jadi "Siap diambil". - Kalau pengambilan ulang gagal (misalnya offline),
datadari cache tetap ada tapiisErrorjaditrue. Tampilkan layar error cuma kalau belum ada data:if (isError && !data). - Cache yang lebih tua dari
maxAge(bawaannya 24 jam) dibuang. Isibusterdengan versi baru saat bentuk datanya berubah. gcTimeharus sama atau lebih besar darimaxAge. Bawaannya 5 menit, jadi tanpa itu cache-nya dibuang setelah 5 menit gak dipakai.
Yang gak boleh disimpan polos
File AsyncStorage dan MMKV adalah teks polos di folder aplikasi. Aplikasi lain gak bisa membacanya, tapi HP yang di-root dan build debug bisa.
- Token login:
expo-secure-store(sudah begitu sejak pelajaran API). - Password: jangan disimpan sama sekali. Simpan token yang bisa dicabut server.
- PIN, OTP, nomor KTP, foto identitas: jangan di AsyncStorage atau MMKV polos. MMKV bisa dienkripsi (
createMMKV({ id: 'rahasia', encryptionKey })), tapi kuncinya sendiri harus dari SecureStore, bukan ditulis di kode. - Saat logout, bersihkan cache:
queryClient.clear()laluawait persister.removeClient(). Kalau gak, akun berikutnya di HP yang sama melihat pesanan Mbak Wulan.
⚠️ Jebakan yang sering ketemu
- Lupa
await.AsyncStorage.getItem('filter-pesanan')tanpaawaitmemberi Promise, bukan string. - Menyimpan object langsung. Nilainya harus string. AsyncStorage 2.2.0 memperingatkan
The value for key "..." is not a string. PakaiJSON.stringifydanJSON.parse, atau biarkancreateJSONStorageyang mengurus. - Token di AsyncStorage atau MMKV. Tempatnya SecureStore.
- MMKV di Expo Go. Gak ada kode native-nya. Buat development build.
gcTimelebih kecil darimaxAge. Cache yang sudah disimpan tetap dibuang oleh garbage collector.
TL;DR: AsyncStorage (2.2.0 di SDK 57) menyimpan string tanpa enkripsi lewat Promise, ada di Expo Go, dan dibatasi 6 MB di Android. MMKV 4 (createMMKV(), butuh react-native-nitro-modules dan development build) membaca secara synchronous lewat JSI, sekitar 30 kali lebih cepat menurut README-nya, jadi filter yang disimpan sudah benar di render pertama, sementara AsyncStorage menampilkan 'Semua' dulu lalu berkedip ke "Disetrika". SecureStore (Keychain dan Keystore) cuma untuk rahasia kecil seperti token. expo-sqlite untuk data banyak, dengan kv-store sebagai pengganti AsyncStorage. Untuk mulai cepat, simpan cache TanStack Query dengan PersistQueryClientProvider dan createAsyncStoragePersister (maxAge 24 jam, gcTime minimal sama). Jangan simpan token, password, PIN, atau data identitas tanpa enkripsi, dan bersihkan cache saat logout.