Local storage is one of the first decisions in any React Native app: persisted settings, onboarding flags, cached API responses, and offline drafts. The two most common answers are AsyncStorage and MMKV. In 2026 the choice is clearer than ever, especially with react-native-mmkv v4 rebuilt on Nitro Modules.
The short answer
- Use MMKV for app settings, feature flags, cached data, and state persistence (Zustand, Redux, TanStack Query). It's synchronous and about 30x faster.
- Use AsyncStorage if you must run in Expo Go or want zero native setup for a prototype.
- Use expo-secure-store for tokens and secrets.
- Use expo-sqlite for relational or queryable data.
Why MMKV is so much faster
AsyncStorage is an asynchronous key/value store. Every read is a promise that crosses into native code, and values are serialized strings.
MMKV (originally built by WeChat) is a memory-mapped native store. react-native-mmkv exposes it through JSI, so reads and writes are synchronous. There's no await, no loading state, and no flash of default theme on startup.
// AsyncStorage
const theme = await AsyncStorage.getItem("theme") // async, needs a loading state
// MMKV
const theme = storage.getString("theme") // sync, available on first render
That synchronous read is the real UX win. You can read the user's theme, locale, or onboarding flag before the first frame renders.
Install MMKV v4 in Expo
MMKV includes native code, so it needs a development build (not Expo Go):
npx expo install react-native-mmkv react-native-nitro-modules
npx expo prebuild
Or build with EAS: eas build --profile development.
Create a storage instance
v4 replaces the new MMKV() constructor with createMMKV(). Create one instance and export it:
// lib/storage.ts
import { createMMKV } from "react-native-mmkv"
export const storage = createMMKV()
// Optional: separate, encrypted instance
export const secureCache = createMMKV({
id: "user-cache",
encryptionKey: "load-this-from-secure-store",
})
Read, write, and delete
import { storage } from "@/lib/storage"
storage.set("user.name", "Ada")
storage.set("user.age", 36)
storage.set("onboarding.done", true)
storage.getString("user.name") // "Ada"
storage.getNumber("user.age") // 36
storage.getBoolean("onboarding.done") // true
storage.contains("user.name") // true
storage.getAllKeys() // ["user.name", ...]
storage.remove("user.name") // v4: renamed from delete()
storage.clearAll()
For objects, use JSON:
storage.set("cart", JSON.stringify(cart))
const cart = JSON.parse(storage.getString("cart") ?? "[]")
React hooks
MMKV ships hooks that re-render when a value changes, which works well for settings:
import { useMMKVBoolean, useMMKVString } from "react-native-mmkv"
export function ThemeToggle() {
const [theme, setTheme] = useMMKVString("theme")
const [haptics, setHaptics] = useMMKVBoolean("haptics")
// setTheme("dark") persists and updates every subscriber
}
Persist Zustand with MMKV
The most common production pattern is to back your state manager with MMKV:
import { create } from "zustand"
import { persist, createJSONStorage, type StateStorage } from "zustand/middleware"
import { storage } from "@/lib/storage"
const mmkvStorage: StateStorage = {
getItem: (name) => storage.getString(name) ?? null,
setItem: (name, value) => storage.set(name, value),
removeItem: (name) => storage.remove(name),
}
export const useSettings = create(
persist<{ theme: "light" | "dark"; setTheme: (t: "light" | "dark") => void }>(
(set) => ({ theme: "light", setTheme: (theme) => set({ theme }) }),
{ name: "settings", storage: createJSONStorage(() => mmkvStorage) },
),
)
Because MMKV is synchronous, the store hydrates instantly, with no "hydrating…" state. More on picking a state library in our React Native state management guide.
Migrating from AsyncStorage
Run a one-time migration on app start:
import AsyncStorage from "@react-native-async-storage/async-storage"
import { storage } from "@/lib/storage"
export async function migrateFromAsyncStorage() {
if (storage.getBoolean("migrated.asyncstorage")) return
const keys = await AsyncStorage.getAllKeys()
for (const key of keys) {
const value = await AsyncStorage.getItem(key)
if (value != null) storage.set(key, value)
}
await AsyncStorage.multiRemove(keys)
storage.set("migrated.asyncstorage", true)
}
MMKV vs AsyncStorage at a glance
- Speed: MMKV is roughly 30x faster in benchmarks.
- API: MMKV is synchronous; AsyncStorage is promise-based.
- Types: MMKV stores strings, numbers, booleans, and ArrayBuffers natively; AsyncStorage stores strings only.
- Encryption: MMKV has built-in AES (
encryptionKey,encrypt(),decrypt()); AsyncStorage has none. - Multiple instances: MMKV supports them via
id; AsyncStorage has a single store. - Expo Go: AsyncStorage works there; MMKV needs a dev build.
Where secrets belong
MMKV encryption protects data at rest, but your encryption key still needs a safe home. Generate it once, store it in expo-secure-store (Keychain or Keystore), and pass it to createMMKV. Keep auth tokens in SecureStore directly. Our Expo authentication guide shows the full pattern.
Want to skip the plumbing? Start from one of our Expo app templates and spend your time on features instead of setup.