This episode covers local storage: AsyncStorage for key-value data, SQLite and MMKV for large data, sensitive data encryption, plus offline-first patterns with a queue and sync strategies.

Mobile apps don't always have a connection. Users ride the train, enter an elevator, or walk in a dead zone — and the app still has to be usable. This is where local persistence comes in: storing data on the device so features keep working offline.
Episode 9 covers the local storage options in React Native: AsyncStorage for key-value data, SQLite and MMKV for large data, encryption for sensitive data, then offline-first patterns with a queue and sync strategies. Episode 13 will go deeper into the security side of credential storage.
AsyncStorage is a simple asynchronous key-value store, suitable for preferences, drafts, and small caches. Install it from the community:
npm install @react-native-async-storage/async-storageimport AsyncStorage from "@react-native-async-storage/async-storage";
const KUNCI = "preferensi_user";
async function simpanPreferensi(nilai) {
await AsyncStorage.setItem(KUNCI, JSON.stringify(nilai));
}
async function bacaPreferensi() {
const mentah = await AsyncStorage.getItem(KUNCI);
return mentah ? JSON.parse(mentah) : null;
}Note AsyncStorage.setItem(KUNCI, JSON.stringify(nilai)): AsyncStorage only stores strings, so objects must be serialized with JSON. Never store tokens or passwords here — that's episode 13's business.
AsyncStorage is suitable for small to medium data: user preferences, themes, unsent form data, or list caches. Its limitation: all data is loaded into memory at once when read, so it's not ideal for large collections or search.
For structured data with search and relations — chat history, offline lists of thousands of items — SQLite is more appropriate. Use expo-sqlite for Expo projects or a native library for CLI:
npx expo install expo-sqliteimport { openDatabaseSync } from "expo-sqlite";
const db = openDatabaseSync("app.db");
db.execSync(
"CREATE TABLE IF NOT EXISTS catatan (id INTEGER PRIMARY KEY, judul TEXT)"
);
db.runSync(
"INSERT INTO catatan (judul) VALUES (?)",
"Catatan pertama offline"
);MMKV (from WeChat) stores data directly in a file without JSON serialization, with optional encryption — far faster than AsyncStorage. It fits when read-write performance is a priority:
npm install react-native-mmkvimport { MMKV } from "react-native-mmkv";
export const storage = new MMKV({
id: "app-storage",
encryptionKey: "kunci-dari-server",
});storage.set("user", data) and storage.getString("user") handle direct storage. Note the encryptionKey — don't store the key inside the bundle, fetch it from secure storage at runtime.
The main rule: public data is fine in ordinary AsyncStorage/MMKV, sensitive data must go in secure storage. For fields that need encryption but you still want MMKV performance, use an MMKV instance with an encryptionKey as in the example above.
Encryption keys should be derived from the user's identity or fetched from the server over a secure channel, not hardcoded. The common pattern: the user logs in, gets a token, and that token becomes the basis for deriving the local encryption key. Episode 13 covers the right place to store these keys.
The offline-first approach: user actions are written to local storage as a queue, then sent to the server when the connection returns. If sending fails, the action stays in the queue to be retried:
import AsyncStorage from "@react-native-async-storage/async-storage";
async function enqueueAksi(aksi) {
const mentah = await AsyncStorage.getItem("queue_aksi");
const antrian = mentah ? JSON.parse(mentah) : [];
antrian.push({ ...aksi, waktu: Date.now() });
await AsyncStorage.setItem("queue_aksi", JSON.stringify(antrian));
}The Date.now() above adds a timestamp so the queue can be ordered when processed. Actions wait in the queue until the connection returns, then are sent one by one.
When the connection returns, process the queue in time order, remove successful actions, and handle conflicts. Add a connection listener with @react-native-community/netinfo so synchronization happens automatically when the isConnected status changes.
Server data stored offline is given version and timestamp metadata. When the app is online, compare with the server and update. Caches that are too old should be removed to save storage — episode 16 will discuss data retention further.
Warning
Offline-first doesn't mean storing all data without limits. Set a policy: how long caches stay valid, which actions may be queued, and what happens on version conflicts. Without a policy, storage fills up and synchronization breaks down.
Episode 9 kept the app useful without a connection: AsyncStorage for key-value, SQLite for complex queries, MMKV for speed, encryption for sensitive data, and offline-first patterns with a queue and automatic synchronization.
Key takeaways:
In the next episode, episode 10, we'll discuss native modules and TurboModules: the difference between the old bridge and the New Architecture, the interop layer, writing your first native module in Kotlin and Swift, and calling it from JavaScript.