react-native-mmkv vs AsyncStorage vs expo-secure-store 2026
react-native-mmkv vs AsyncStorage vs expo-secure-store 2026
TL;DR
Every React Native app needs persistent local storage — user preferences, cached data, auth tokens, offline state. AsyncStorage is the built-in default — async key-value store backed by SQLite/SharedPreferences, simple API, but slow (everything is async, no synchronous reads). react-native-mmkv is the performance upgrade — C++ implementation of WeChat's MMKV, synchronous reads, 10x faster than AsyncStorage, optional AES encryption, built-in TypeScript support. expo-secure-store uses the platform's secure enclave — iOS Keychain and Android Keystore, hardware-backed encryption for sensitive data like auth tokens and API keys. For user preferences and app state: MMKV. For sensitive credentials (tokens, keys): expo-secure-store. For simple caching in older Expo Go setups: AsyncStorage.
Key Takeaways
- MMKV is 10x faster than AsyncStorage — synchronous reads, C++ implementation
- expo-secure-store uses hardware encryption — iOS Keychain, Android Keystore
- AsyncStorage is limited to 6MB by default on Android — MMKV and SecureStore have no practical size limits
- MMKV supports synchronous reads —
storage.getString("key")returns immediately - expo-secure-store is always async — Keychain operations can't be synchronous
- MMKV has optional AES encryption — encrypt the entire storage file with a key
- AsyncStorage is deprecated in Expo Go — React Native's own recommendation is to migrate away
Storage Use Cases
Auth tokens (JWT, refresh tokens)
→ expo-secure-store (hardware-encrypted, small data)
User preferences (theme, language, notifications)
→ react-native-mmkv (fast reads, frequently accessed)
Cached API responses (large JSON objects)
→ react-native-mmkv or AsyncStorage (size matters)
Draft data (unsent messages, forms)
→ react-native-mmkv (synchronous saves, fast)
Large files / binary data
→ expo-file-system (neither KV store is ideal)
Session state (cart, scroll position)
→ react-native-mmkv + Zustand persist middleware
AsyncStorage: The Standard Baseline
AsyncStorage is the React Native community's standard async key-value storage. It's well-supported, works everywhere including Expo Go, but has performance limitations.
Installation
npx expo install @react-native-async-storage/async-storage
Basic Usage
import AsyncStorage from "@react-native-async-storage/async-storage";
// Store data
await AsyncStorage.setItem("theme", "dark");
await AsyncStorage.setItem("user", JSON.stringify({ id: "123", name: "Alice" }));
// Read data
const theme = await AsyncStorage.getItem("theme");
const userJson = await AsyncStorage.getItem("user");
const user = userJson ? JSON.parse(userJson) : null;
// Remove
await AsyncStorage.removeItem("theme");
// Batch operations (more efficient than individual calls)
await AsyncStorage.multiSet([
["theme", "dark"],
["language", "en"],
["notifications", "true"],
]);
const [theme, language] = await AsyncStorage.multiGet(["theme", "language"]);
Wrapper with Type Safety
// Type-safe wrapper around AsyncStorage
class TypedStorage {
static async set<T>(key: string, value: T): Promise<void> {
await AsyncStorage.setItem(key, JSON.stringify(value));
}
static async get<T>(key: string): Promise<T | null> {
const item = await AsyncStorage.getItem(key);
if (item === null) return null;
try {
return JSON.parse(item) as T;
} catch {
return item as unknown as T;
}
}
static async remove(key: string): Promise<void> {
await AsyncStorage.removeItem(key);
}
}
// Usage
await TypedStorage.set("preferences", { theme: "dark", language: "en" });
const prefs = await TypedStorage.get<{ theme: string; language: string }>("preferences");
AsyncStorage with Zustand
import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
import AsyncStorage from "@react-native-async-storage/async-storage";
const storage = createJSONStorage(() => AsyncStorage);
export const usePreferencesStore = create(
persist(
(set) => ({
theme: "system" as "light" | "dark" | "system",
language: "en",
setTheme: (theme: "light" | "dark" | "system") => set({ theme }),
}),
{
name: "user-preferences",
storage,
}
)
);
react-native-mmkv: High-Performance Storage
MMKV is Tencent WeChat's production key-value store — it uses mmap for near-instant reads and writes, with optional AES encryption of the entire storage file.
Installation
npx expo install react-native-mmkv
# Requires Expo bare workflow or React Native CLI
# Does NOT work in Expo Go (requires native module)
Basic Usage (Synchronous)
import { MMKV } from "react-native-mmkv";
// Create storage instance
export const storage = new MMKV();
// Synchronous reads and writes — no await needed
storage.set("theme", "dark");
storage.set("userId", 12345);
storage.set("isLoggedIn", true);
storage.set("preferences", JSON.stringify({ language: "en" }));
// Synchronous reads
const theme = storage.getString("theme"); // Returns string | undefined
const userId = storage.getNumber("userId"); // Returns number | undefined
const isLoggedIn = storage.getBoolean("isLoggedIn"); // Returns boolean | undefined
// Delete
storage.delete("theme");
// Check existence
const hasTheme = storage.contains("theme");
// Get all keys
const allKeys = storage.getAllKeys(); // string[]
// Clear all
storage.clearAll();
Encrypted Storage
import { MMKV } from "react-native-mmkv";
// AES-256 encrypted storage
export const secureStorage = new MMKV({
id: "secure-storage",
encryptionKey: "your-32-character-encryption-key!!", // 32 chars for AES-256
});
// Or derive key from device-specific secret
import * as SecureStore from "expo-secure-store";
async function createEncryptedStorage() {
// Generate or retrieve encryption key from SecureStore
let key = await SecureStore.getItemAsync("mmkv-encryption-key");
if (!key) {
key = generateRandomKey(32);
await SecureStore.setItemAsync("mmkv-encryption-key", key);
}
return new MMKV({
id: "encrypted-app-storage",
encryptionKey: key,
});
}
Multiple Storage Instances
import { MMKV } from "react-native-mmkv";
// Separate namespaced storage instances
export const appStorage = new MMKV({ id: "app-storage" });
export const cacheStorage = new MMKV({ id: "cache-storage" });
export const userStorage = new MMKV({ id: "user-storage" });
// Clear cache without affecting app or user data
cacheStorage.clearAll();
MMKV with Zustand (Synchronous Persist)
import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
import { storage } from "@/lib/storage"; // MMKV instance
// Synchronous Zustand storage adapter for MMKV
const mmkvStorage = createJSONStorage(() => ({
setItem: (name: string, value: string) => storage.set(name, value),
getItem: (name: string) => storage.getString(name) ?? null,
removeItem: (name: string) => storage.delete(name),
}));
export const useCartStore = create(
persist(
(set, get) => ({
items: [] as CartItem[],
addItem: (item: CartItem) => set((state) => ({
items: [...state.items, item],
})),
total: () => get().items.reduce((sum, item) => sum + item.price, 0),
}),
{
name: "cart-storage",
storage: mmkvStorage,
}
)
);
Performance: MMKV vs AsyncStorage
Benchmark: 1,000 string write/read cycles
AsyncStorage:
- Write: ~2,500ms total (~2.5ms per op)
- Read: ~1,800ms total (~1.8ms per op)
MMKV:
- Write: ~45ms total (~0.045ms per op)
- Read: ~12ms total (~0.012ms per op)
MMKV is ~55x faster on writes, ~150x faster on reads
expo-secure-store: Hardware-Encrypted Storage
expo-secure-store uses the platform's secure hardware — iOS Keychain Services and Android Keystore — for storing sensitive values like auth tokens, API keys, and private data.
Installation
npx expo install expo-secure-store
# Works in Expo Go and bare workflow
Basic Usage
import * as SecureStore from "expo-secure-store";
// Store sensitive data (async, hardware-backed)
await SecureStore.setItemAsync("auth_token", accessToken);
await SecureStore.setItemAsync("refresh_token", refreshToken);
await SecureStore.setItemAsync("api_key", apiKey);
// Read
const token = await SecureStore.getItemAsync("auth_token");
if (!token) throw new Error("Not authenticated");
// Delete
await SecureStore.deleteItemAsync("auth_token");
// Check if available on device
const isAvailable = await SecureStore.isAvailableAsync();
Options: Accessibility and Biometrics
// Require biometric authentication to access the value
await SecureStore.setItemAsync("sensitive_key", value, {
requireAuthentication: true, // FaceID / TouchID / Biometric required
authenticationPrompt: "Verify your identity to access your data",
});
// Control when the value is accessible on iOS
await SecureStore.setItemAsync("token", value, {
keychainAccessible: SecureStore.WHEN_UNLOCKED, // Default
// SecureStore.AFTER_FIRST_UNLOCK — available after first unlock
// SecureStore.ALWAYS — always available (less secure)
// SecureStore.WHEN_PASSCODE_SET_THIS_DEVICE_ONLY
// SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY
});
Auth Token Management Pattern
// lib/auth-storage.ts — Centralized secure token storage
import * as SecureStore from "expo-secure-store";
const ACCESS_TOKEN_KEY = "access_token";
const REFRESH_TOKEN_KEY = "refresh_token";
const USER_ID_KEY = "user_id";
export const authStorage = {
async saveTokens(accessToken: string, refreshToken: string, userId: string) {
await Promise.all([
SecureStore.setItemAsync(ACCESS_TOKEN_KEY, accessToken),
SecureStore.setItemAsync(REFRESH_TOKEN_KEY, refreshToken),
SecureStore.setItemAsync(USER_ID_KEY, userId),
]);
},
async getAccessToken(): Promise<string | null> {
return SecureStore.getItemAsync(ACCESS_TOKEN_KEY);
},
async getRefreshToken(): Promise<string | null> {
return SecureStore.getItemAsync(REFRESH_TOKEN_KEY);
},
async clearTokens() {
await Promise.all([
SecureStore.deleteItemAsync(ACCESS_TOKEN_KEY),
SecureStore.deleteItemAsync(REFRESH_TOKEN_KEY),
SecureStore.deleteItemAsync(USER_ID_KEY),
]);
},
async isAuthenticated(): Promise<boolean> {
const token = await SecureStore.getItemAsync(ACCESS_TOKEN_KEY);
return token !== null;
},
};
Feature Comparison
| Feature | AsyncStorage | MMKV | expo-secure-store |
|---|---|---|---|
| Read performance | Slow (async) | ✅ Synchronous, fast | Slow (async) |
| Encryption | ❌ | ✅ Optional AES | ✅ Hardware (Keychain/Keystore) |
| Synchronous API | ❌ | ✅ | ❌ |
| Size limit (Android) | 6MB default | No limit | 2KB per key |
| Expo Go | ✅ | ❌ | ✅ |
| New Architecture | ✅ | ✅ | ✅ |
| Multiple instances | ❌ | ✅ | ❌ |
| Biometric lock | ❌ | ❌ | ✅ |
| Binary data | ❌ | ✅ | ❌ |
| GitHub stars | 4.5k | 8.5k | (Expo SDK) |
When to Use Each
Choose AsyncStorage if:
- Your app uses Expo Go (no native module compilation)
- Simple async key-value needs where performance isn't critical
- You're migrating an existing app incrementally and MMKV isn't yet worth the setup
Choose react-native-mmkv if:
- Performance matters — preferences read in component render, frequent writes
- You want synchronous access patterns without
useEffect/async complications - Replacing AsyncStorage in a Zustand or Jotai persist store
- You need multiple isolated storage namespaces
Choose expo-secure-store if:
- Storing auth tokens, API keys, refresh tokens, or any sensitive credentials
- Biometric authentication before accessing stored values is needed
- Hardware-backed encryption (Keychain/Keystore) is a compliance requirement
- Small amounts of sensitive data (< 2KB per key) — not for large objects
Methodology
Data sourced from the react-native-mmkv GitHub repository and documentation (github.com/mrousavy/react-native-mmkv), AsyncStorage documentation (react-native-async-storage.github.io), expo-secure-store documentation (docs.expo.dev/versions/latest/sdk/securestore), performance benchmarks from Marc Rousavy's MMKV blog posts, and community discussions in the Expo Discord. GitHub star counts as of February 2026.
Related: Zustand vs Jotai vs Nano Stores for the state management layer that uses these storage solutions for persistence, or NativeWind vs Tamagui vs twrnc for other React Native tooling decisions.