The Complete Guide to Offline Storage in React Native (2026)

javascript dev.to
  • 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

  1. Key-valueAsyncStorage (compat) or MMKV (default). MMKV is ~30× faster and supports synchronous reads.
  2. Secureexpo-secure-store or react-native-keychain. Backed by iOS Keychain / Android Keystore.
  3. Structuredexpo-sqlite, op-sqlite (JSI, synchronous), WatermelonDB (reactive + sync), Realm (Atlas Device Sync).
  4. File systemexpo-file-system for media. Never base64 blobs into KV storage.
  5. Server-state cache — TanStack Query with persistQueryClient, backed by MMKV.

Comparison table

Library Type Perf Encryption Best for
AsyncStorage KV 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:

  1. Local-first reads. The UI always reads from the local store. The network populates the store; the store renders the UI.
  2. Optimistic writes. The UI updates immediately, before the server acknowledges.
  3. Mutation queue. A durable list (SQLite or MMKV) of pending server-side changes, each with a client ID.
  4. 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();
}
Enter fullscreen mode Exit fullscreen mode

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 persistQueryClient on MMKV
  • Thousands of offline records? → op-sqlite, or WatermelonDB if you want reactive
  • Cross-device sync? → WatermelonDB or Realm
  • Media? → expo-file-system with metadata in SQLite

Pitfalls to skip

  • Blocking the JS thread with async AsyncStorage calls 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.

Source: dev.to

arrow_back Back to Tutorials