react-brain
Browse decisions
React & language foundations 3App architecture 10UI 13Platform & native 9Build, test, observe, secure 6AI in React apps 3stack composerdecision recordscensusstaleness bencharchitecturechangelogmethodologyroadmap

entriesplatform-native · verified 2026-08-18 · react-native

On-device storage & persistence

reviewedconfidence: mediumtrack: weakenedthis tier holds 67% on the public scorecard (6/19 graded · 0 overturned) →

related decisions: p2p

cited by: auth · security

re-verified 4× — 2026-08-18 · 2026-08-04 · 2026-07-13 · 2026-07-06 · changelog

recommendation

Small key-value → react-native-mmkv (fast, synchronous). Relational/large → op-sqlite or expo-sqlite. Secrets/tokens → the OS Keychain/Keystore, never AsyncStorage — on Expo that is expo-secure-store, on bare RN react-native-keychain.

  • simple persisted prefs, no perf concern → AsyncStorage is fine
  • frequent synchronous reads → MMKV
  • structured/queryable data → SQLite (op-sqlite for perf)
  • secrets on an Expo app → expo-secure-store (first-party, released with the SDK); reach for react-native-keychain when you need access-control options it doesn't expose — and note keychain has not published since 2025-03-23
  • P2P / local-first app → primary persistence is the Hypercore/Autobase/Hyperbee log, not KV/SQLite (KV like AsyncStorage is just for local prefs) — see RB-E-P2P

Options & tradeoffs

the field considered — and why each one isn’t the default here

optiontradeoffevidence
@react-native-async-storage/async-storagesimple async key-value; v3.x is instance-based (createAsyncStorage), getMany/setMany/removeMany, requires RN 0.76+; v2 default export kept for back-compat
react-native-mmkv (v4)fast synchronous key-value; v4 is a full Nitro Modules rewrite (restores Old-Arch compat); common AsyncStorage replacement1.3M/wk · ships in 6/14
op-sqlite / expo-sqliterelational SQLite; op-sqlite is high-perf JSI127k/wk
expo-secure-store (57.0.1)secure storage for tokens/secrets on the Keychain/Keystore; first-party, ships with the Expo SDK and releases with it (2026-07-15) — the default secrets store on any Expo app4.3M/wk · ships in 1/14
react-native-keychain (10.0.0)the bare-RN secrets store, and the one to choose when you need finer access-control/biometry configuration than secure-store exposes. QUIET: no npm release since 2025-03-23 (17 months), 180 open issues, though the repo still gets commits (pushed 2026-04-29) — pin it and re-check before starting new work on it449k/wk

evidence: npm weekly downloads (signals snapshot) · “ships in n/D” = adoption across the production-app census, honest denominators

npm weekly downloads (from the corpus's last signals run): react-native-mmkv 1.3M · @react-native-async-storage/async-storage 4.7M · @op-engineering/op-sqlite 127k · expo-sqlite 841k · react-native-keychain 449k · expo-secure-store 4.3M · @nozbe/watermelondb 54k

Verified notes

AsyncStorage v3.x moved to instance-based storage (createAsyncStorage) and requires React Native 0.76+ (NOT New-Arch-only — that earlier claim was wrong; verified vs the v3 migration docs). react-native-mmkv v4 is a full Nitro rewrite (Oct 2025) that restored Old-Arch compat. Latest: async-storage 3.1.x, mmkv 4.3.x, op-sqlite 18.1.1 (2026-08-17). OP-SQLITE 18 IS BREAKING (2026-08-14, verified vs the GitHub releases): 18.0.0 REMOVES crsqlite — if you were using op-sqlite as the local half of a CRDT-synced store, that extension is gone and the migration is yours; it also rebinds hooks/reactive flush to their runtime generation and drops an internal mutex in favour of self-cleaning databases. 18.1 adds initial Swift Package Manager support and RN 0.87 compatibility, so on 0.87 this is the line to be on.

Canonical reading

Editorial annotations on why each piece matters — the articles themselves are the originals; read them there.

SQLite for React Native, but 5x faster and 5x less memoryOscar Franco (op-sqlite author)

The library author walks the JSI/C++ internals that make op-sqlite fast and memory-light — lazy HostObject conversion, std::variant over a custom struct, key-sharing across result rows — with benchmarks. The mechanics of high-performance JSI SQLite, not API usage.

An Interactive Guide to TanStack DBMaxi Ferreira (Frontend at Scale)

Builds the local-first reactive-persistence mental model from first principles — Collections, live queries via differential dataflow, optimistic transactional mutations — contrasting it with request/response fetching. The shift to sync-engine thinking.

Why We Did Not Move MMKV Writes to a Worklet: The Serialization CostAndrei Calazans

A measured NEGATIVE result, which is why it is worth holding: MMKV writes are synchronous and block the JS thread, so moving them into a worklet looks like free relief. It isn't. Moving just the write left the ~29.56ms cost intact — it simply moved from the write into createSerializableString, because copying the string into the worklet costs what the write did. Sending the raw store object instead was worse (~132ms, cloning every property one at a time via cloneObjectProperties/clonePlainJSObject), and Bundle Mode changed nothing (~136ms), confirming the cost tracks data-structure size rather than compilation. The durable rule: a worklet is not a free way to move work off the JS thread — you pay serialization at the boundary, so measure that cost first and send the smallest payload you can.

Sources

The full explanation

The reviewed long-form essay behind this entry — the why, not a how-to. Also on GitHub.

About on-device storage & persistence

Diataxis: Explanation. This page builds understanding of how to place data on the device — the reasoning behind the picks. It is not an API guide or a migration runbook. Local-first / P2P persistence (Hypercore, Autobase, Hyperbee) is owned by RB-E-P2P; secrets policy depth by RB-E-SECURITY; server-state caching by RB-E-DATA. Read this for the why.

The one principle that organises everything: pick storage by data shape × secrecy

"Where do I store this?" is really three different questions wearing one trenchcoat: small key-value (prefs, flags, last-seen timestamps), relational or large structured data (anything you query), and secrets (tokens, credentials). Each has a different home because each optimizes a different thing — read latency, queryability, and OS-level protection respectively. The classic failure is letting one tool answer all three: AsyncStorage as prefs store, database, and token vault. It's genuinely fine at the first job, wrong for the second, and the recommendation is explicit about the third — never for secrets. Classify by shape and secrecy first; the tool picks itself.

The default, and why

Small key-value → react-native-mmkv (fast, synchronous). Relational/large → op-sqlite or expo-sqlite. Secrets/tokens → Keychain / secure-store, never AsyncStorage.

Three shapes, three homes. MMKV wins small key-value because it's fast and synchronous — no await ceremony for a boolean. SQLite wins structured data because "relational/large" is a query problem, and op-sqlite attacks it with high-perf JSI. Keychain/Keystore wins secrets because secrecy is an OS-protection problem, not a persistence problem — which is why the default ends with "never AsyncStorage" for tokens. AsyncStorage isn't banished: for simple persisted prefs with no perf concern, the when-clause says it's fine.

The landscape, and when each piece earns its place

@react-native-async-storage/async-storage — simple async key-value. v3.x moved to instance-based storage (createAsyncStorage) with getMany/setMany/removeMany, and requires RN 0.76+; the v2 default export is kept for back-compat. Latest is 3.1.x. Earns its place for simple persisted prefs when perf isn't a concern.

react-native-mmkv (v4) — fast synchronous key-value, the common AsyncStorage replacement. v4 is a full Nitro Modules rewrite (Oct 2025) that restored Old-Architecture compatibility. Latest is 4.3.x. Earns its place the moment you have frequent synchronous reads.

op-sqlite / expo-sqlite — relational SQLite for structured/queryable data; op-sqlite is the high-perf JSI option (latest 17.x, 2026-06). The author's own deep-dive explains why it's fast and memory-light: lazy HostObject conversion, std::variant over a custom struct, key-sharing across result rows — with benchmarks.

react-native-keychain / expo-secure-store — secure storage for tokens and secrets, backed by the platform Keychain/Keystore. Earns its place for every credential, unconditionally.

The P2P/local-first log — in a Hypercore/Autobase app, the primary persistence is the Hypercore/Autobase/Hyperbee log itself, not KV or SQLite; KV like AsyncStorage is just for local prefs there. That whole world is RB-E-P2P's.

Tradeoffs and failure modes to name out loud

  • Tokens in AsyncStorage. The one "never" in the default. Secrets need Keychain/Keystore protection; a key-value store is the wrong trust boundary no matter how convenient.
  • AsyncStorage as a database. Structured/queryable data belongs in SQLite. Key-value stores answer "get me X by key", not queries.
  • Misreading the AsyncStorage v3 requirements. v3.x requires RN 0.76+ — but it is not New-Arch-only; that earlier claim was wrong (verified against the v3 migration docs). The v2 default export is kept for back-compat, so the instance-based createAsyncStorage move is a migration, not a cliff.
  • Avoiding MMKV v4 over Old-Arch fears. Backwards: the v4 Nitro rewrite is what restored Old-Arch compat.
  • Giving a local-first app a "main database". In P2P apps the log is the source of truth; making SQLite/KV primary builds a second, divergent one. KV is for prefs only there.
  • Persisting like it's still request/response. The TanStack DB reading marks the shift to sync-engine thinking — Collections, live queries via differential dataflow, optimistic transactional mutations — a different mental model from fetch-then-cache.

How it interacts with the rest of the stack

  • P2P (RB-E-P2P). The when-clause draws the line: local-first apps persist primarily in the Hypercore/Autobase/Hyperbee log; this entry's tools shrink to a prefs sidecar.
  • Security (RB-E-SECURITY). The secrets row is where the two entries meet — Keychain/ Keystore is an OS security boundary, and "never AsyncStorage" for tokens is as much a security rule as a storage one.
  • Data layer (RB-E-DATA). On-device persistence is the other half of the data story: server-state caching lives there, and the TanStack DB reading contrasts local-first reactive persistence with request/response fetching — the boundary both entries share.

In one paragraph

On-device storage is three problems, not one — classify by data shape × secrecy and each picks its own home: small key-value goes to react-native-mmkv (fast, synchronous; v4 is a Nitro rewrite that restored Old-Arch compat), relational/large data goes to op-sqlite or expo-sqlite (op-sqlite for high-perf JSI), and secrets go to Keychain / secure-store — never AsyncStorage. AsyncStorage itself remains fine for simple persisted prefs (v3.x is instance-based and needs RN 0.76+, with the v2 export kept for back-compat), and in P2P local-first apps the whole question shifts: the Hypercore/Autobase/Hyperbee log is the primary persistence, and everything on this page is just the prefs drawer.


See also: RB-E-P2P (the log as primary persistence in local-first apps), RB-E-SECURITY (secrets belong to the OS boundary), RB-E-DATA (server-state caching; sync-engine thinking vs request/response).

Related in platform-native: native · media · native-ui · keyboard · payments · brownfield · games · alt-frameworks