React Native Async Storage: What to Use Instead
React Native AsyncStorage is the default key-value store, but for many production apps it’s not enough. Here’s when to replace it and how to pick the right alternative—step by step.

React Native AsyncStorage is the built‑in key‑value store that ships with every React Native project. It works, but it is a string‑only map with no size guarantees and zero encryption. For settings or a small cache, that’s fine. When your users care about performance, data fidelity, or security, you need something else. In this guide, we’ll move through exactly which option to pick—and how to migrate—with real code you can copy.
When React Native AsyncStorage Still Makes Sense
AsyncStorage is not going away, and it still earns its keep for the simplest cases. If you’re storing:
- A handful of user preferences (theme, onboarding flag, language selection)
- Non‑sensitive tokens that you can afford to lose (like a cached list of recently viewed items)
- Booleans or small strings that never need querying
… then AsyncStorage is a zero‑dependency win. The API is straightforward, it’s async, and it works on every platform. A typical read looks like this:
const value = await AsyncStorage.getItem('@preferences_language');As soon as your data grows beyond a handful of kilobytes, or you need to search for a specific object inside a set, AsyncStorage becomes a liability.
Why AsyncStorage Isn’t Enough for Production Apps
AsyncStorage is a flat string map. That means:
- No size promises – the underlying SQLite/Misc nature means there’s no contractual limit, but also no guarantee your data won’t be evicted when the device is under pressure.
- No encryption – strings are stored in plaintext, accessible to anyone with a rooted device or a backup extraction. For HIPAA, PCI, or even a user token, that’s a non‑starter.
- String‑only – every value must be
JSON.stringify’d and parsed. Complex nested objects are fragile, and type safety is manual. - No querying – you can’t ask “give me all items updated in the last week”. You must load the entire key and deserialize, which is slow on large sets.
- Sync bottlenecks – the async API is fine for a few reads, but under a write‑heavy workload, the queueing can cause UI jank.
Every app eventually grows out of these constraints. The real question: which exit ramp do you take?
MMKV: The Drop‑in Speed Upgrade
If your main pain point is speed and you still want a key‑value model, MMKV is the go‑to replacement. It’s backed by memory‑mapped files from Tencent, so reads are synchronous and writes are nearly instant, even with thousands of keys.
Setup (bare React Native or Expo with dev‑client):
npm install react-native-mmkv
# For Expo, also run: npx expo install expo-dev-clientimport { MMKV } from 'react-native-mmkv';
export const storage = new MMKV({
id: 'user-global',
encryptionKey: 'optional-but-recommended-key',
});Read and write replace AsyncStorage directly:
// AsyncStorage:
await AsyncStorage.setItem('@score', JSON.stringify(100));
const score = JSON.parse(await AsyncStorage.getItem('@score') || '0');
// MMKV (sync read, no parsing):
storage.set('score', 100);
const score = storage.getNumber('score') ?? 0;MMKV gives you native performance, typed getters (getBoolean, getString, getBuffer), and optional AES encryption. It’s the option we reach for first at techpotions when a client’s settings store becomes a bottleneck and they need to ship immediately.
Adding a native module like MMKV does increase your binary size. Learn how to keep your release lean in our guide on shrinking React Native app size.
Expo SQLite: Relational Storage Without a Backend
The moment you need to ask questions about your data—sort, filter, aggregate—you need a database, not a key‑value store. Expo SQLite gives you a full local SQL engine that runs directly on the device, perfect for offline‑first workflows.
Setup:
npx expo install expo-sqliteCreate a table and insert with parameterised queries:
import * as SQLite from 'expo-sqlite';
const db = await SQLite.openDatabaseAsync('offline.db');
await db.execAsync(`
PRAGMA journal_mode = WAL;
CREATE TABLE IF NOT EXISTS drafts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
content TEXT NOT NULL,
updated_at INTEGER DEFAULT (strftime('%s','now'))
);
`);
await db.runAsync(
'INSERT INTO drafts (content) VALUES (?)',
'The meeting follow-up notes...'
);Query with filters that AsyncStorage could never handle:
const recentDrafts = await db.getAllAsync(
'SELECT * FROM drafts WHERE updated_at > ? ORDER BY updated_at DESC',
Date.now() - 7 * 24 * 60 * 60 * 1000
);expo‑sqlite is the right call when your offline functionality is not just a cache but the primary source of truth that later syncs to a server. It handles gigabytes of structured data without sweating, and the migration story is just a few ALTER TABLE statements.
If syncing logic is the part you dread, techpotions’ mobile services can design a storage layer that merges, resolves conflicts, and never loses a region‑specific draft.
Encryption: Protecting Data That AsyncStorage Leaves Open
AsyncStorage stores everything in plaintext. For credentials, session tokens, or any PII, you need hardware‑backed storage.
- expo‑secure‑store uses Android Keystore and iOS Keychain, guaranteeing that even a device backup doesn’t leak your secrets. It is a key‑value store limited to small values (usually < 2KB), perfect for access tokens and encryption salts.
- MMKV with an encryption key gives you a larger, faster, encrypted key‑value store that can hold megabytes of structured data behind AES.
In practice, a common pattern is to put sensitive tokens into SecureStore and everything else into an encrypted MMKV instance. The two complement each other without overlapping.
For teams that need to weigh these options quickly, the React Native storage comparison by PkgPulse is a useful snapshot, though note that it focuses on key‑value stores and doesn’t cover relational requirements.
Migrating from AsyncStorage: A Step‑by‑Step Playbook
Switching storage engines mid‑flight is the part that makes engineers nervous. Here’s a phased approach we use at techpotions that avoids data loss and keeps the app stable through an OTA update.
Phase 1: Dual‑write every change
First, let both AsyncStorage and the new store receive writes. Ship this behind a feature flag.
// migration-store.ts
import AsyncStorage from '@react-native-async-storage/async-storage';
import { MMKV } from 'react-native-mmkv';
const mmkv = new MMKV({ id: 'migration' });
export async function setPreference(key: string, value: string) {
// AsyncStorage (existing)
await AsyncStorage.setItem(key, value);
// MMKV (new)
mmkv.set(key, value);
}Phase 2: Back‑fill existing data on app launch
When the user upgrades, migrate all AsyncStorage keys to the new store. This one‑time script runs once inside a splash‑screen gate.
import { MMKV } from 'react-native-mmkv';
const mmkv = new MMKV({ id: 'migration' });
async function fillExistingData() {
const keys = await AsyncStorage.getAllKeys();
for (const key of keys) {
const value = await AsyncStorage.getItem(key);
if (value !== null) mmkv.set(key, value);
}
}Phase 3: Switch reads then drop writes
Once analytics confirm that the new store has all required keys, flip all read operations to the new store. Ship that for a week, then remove the dual‑write and finally the AsyncStorage dependency. The lesson: always keep the migration reversible until you’ve verified real‑world data integrity.
Choosing Your Stack: A Bird’s‑Eye Comparison
Storage | Best For | Data Model | Query Support | Encryption | Write Volume | Migration Complexity |
|---|---|---|---|---|---|---|
AsyncStorage | Simple preferences, tokens you don’t mind losing | String key‑value | None | None | Low | None (already present) |
MMKV | High‑speed key‑value, typed access, settings up to several MB | Key‑value (number, string, bool, buffer) | None built‑in, optional AES key | Per‑instance AES encryption | High (sync) | Low (drop‑in replacement) |
expo‑sqlite | Offline‑first, structured data, relational queries | Tables, rows, SQL | Full SQL | Via sqlcipher if self‑compiled, not bundled | Medium‑high | File‑level encryption possible | Medium (schema management) |
SecureStore | Access tokens, cryptographic keys, small secrets | Key‑value (<2 KB) | None | Hardware‑backed (Keystore/Keychain) | Very low | Low |
When runxbuild evaluated database options for Expo, they noted that AsyncStorage is “fine for small amounts of data”—and that’s still the advice in a nutshell. The moment your data model has any structure, you’re already past its usefulness.
FAQ
Is AsyncStorage deprecated?
No—AsyncStorage is still maintained and bundled with React Native, but it remains a simple, unencrypted string map. For anything beyond basic configuration or small preference sets, you will eventually need something with type safety, queries, or encryption.
Can I use MMKV with Expo?
Yes, MMKV works with Expo when you use a development build or expo-dev-client. It does not run inside Expo Go because it requires native code, but it is a common companion in modern Expo projects that need a fast key‑value store.
What’s the best local storage for an offline‑first chat app?
An offline‑first chat app typically needs real‑time sync and structured querying. expo‑sqlite provides the relational backbone locally, while MMKV can cache small blobs like draft messages. Both can be paired with a server‑side sync engine that techpotions can help design to avoid data loss during reconnect.