- Offline storage is five categories, not one library: key-value, secure, structured, file system, and server-state cache.
- MMKV is the new default over AsyncStorage — roughly 30× faster with synchronous reads.
- The offline-first pattern has four parts: local-first reads, optimistic writes, a durable mutation queue, and a sync engine.
- Conflict resolution: last-write-wins is fine for most consumer apps. CRDTs are overkill unless you're building a collaborative editor.
Most React Native apps treat the network as an always-on dependency, and it shows the moment a request stalls.
Offline storage isn't one library — it's a stack: key-value stores for preferences, secure enclaves for tokens, structured DBs for domain data, and query caches for server state.
This post walks through the whole stack and the offline-first pattern that ties it together.
The five categories of RN offline storage
-
Key-value —
AsyncStorage(compat) orMMKV(default). MMKV is ~30× faster and supports synchronous reads. -
Secure —
expo-secure-storeorreact-native-keychain. Backed by iOS Keychain / Android Keystore. -
Structured —
expo-sqlite,op-sqlite(JSI, synchronous), WatermelonDB (reactive + sync), Realm (Atlas Device Sync). -
File system —
expo-file-systemfor media. Never base64 blobs into KV storage. -
Server-state cache — TanStack Query with
persistQueryClient, backed by MMKV.
Comparison table
| Library | Type | Perf | Encryption | Best for |
|---|---|---|---|---|
| AsyncStorage | KV | 1× | Community fork | Legacy / compat |
| MMKV | KV | ~30× | Built-in | New default |
| expo-secure-store | Secure KV | — | OS keychain | Tokens |
| op-sqlite | SQL | JSI | SQLCipher | Structured data |
| WatermelonDB | Reactive DB | Lazy | SQLCipher | Large offline lists + sync |
| Realm | Object DB | Fast | Yes | Atlas Device Sync |
The offline-first pattern
Four moving parts:
- Local-first reads. The UI always reads from the local store. The network populates the store; the store renders the UI.
- Optimistic writes. The UI updates immediately, before the server acknowledges.
- Mutation queue. A durable list (SQLite or MMKV) of pending server-side changes, each with a client ID.
- Sync engine. A background worker drains the queue with exponential backoff and reconciles conflicts.
async function markMessageAsRead(id: string) {
db.write(() => {
db.messages.update(id, { readAt: Date.now(), pendingSync: true });
});
await mutationQueue.enqueue({
kind: 'message.markRead',
payload: { id, readAt: Date.now() },
clientId: uuid(),
});
syncEngine.wake();
}
Conflict resolution
- Last-write-wins. Timestamps, newest wins. Fine for most consumer apps.
- Server-authoritative. Reject stale writes with a version mismatch.
- CRDTs. Yjs or Automerge. Overkill unless you're building a collaborative editor.
Decision cheat sheet
- Preferences or JWT? → MMKV +
expo-secure-store - Instant-open cached lists? → TanStack Query with
persistQueryClienton MMKV - Thousands of offline records? →
op-sqlite, or WatermelonDB if you want reactive - Cross-device sync? → WatermelonDB or Realm
- Media? →
expo-file-systemwith metadata in SQLite
Pitfalls to skip
- Blocking the JS thread with async
AsyncStoragecalls at startup - Storing blobs in KV stores
- No cache wipe on logout
- Infinite retry loops in the mutation queue
- Persisting Redux state without a schema version
Wrap-up
Offline-first isn't a defensive posture — it's a performance strategy. Reading from local storage beats a round-trip every time, and a good sync engine hides the network from your users entirely.
Pick each layer deliberately and this becomes a solved problem rather than a recurring one.
I built RapidNative, an AI mobile app builder that generates production-ready React Native code you can extend with any of these libraries.
Anyone running MMKV + TanStack Query in production? Curious how the persistence layer has held up for you at scale.
Top comments (1)
I found the discussion on the five categories of offline storage in React Native to be particularly insightful, especially the distinction between key-value stores like MMKV and secure enclaves like expo-secure-store. The performance difference between MMKV and AsyncStorage is striking, with MMKV being roughly 30× faster, which could significantly impact the user experience. The offline-first pattern outlined, with its four parts - local-first reads, optimistic writes, a durable mutation queue, and a sync engine - provides a comprehensive approach to handling offline storage. I'm curious to know more about how others have implemented the sync engine in their applications, particularly in terms of handling conflict resolution and exponential backoff.