Skip to main content

Guide

React Native MMKV vs AsyncStorage vs Expo SecureStore 2026

MMKV, AsyncStorage, and Expo SecureStore for React Native local storage: speed, Expo support, encryption, offline state, and 2026 fit.

·PkgPulse Team·
0
Hero image for React Native MMKV vs AsyncStorage vs Expo SecureStore 2026

TL;DR

For most React Native apps in 2026, the decision is not one winner. Use react-native-mmkv for fast app state, preferences, feature flags, small caches, and Zustand/Jotai persistence; use expo-secure-store for auth tokens, refresh tokens, API keys, and other small secrets; keep AsyncStorage only when you need the simplest async key-value baseline, Expo Go compatibility, or a low-risk migration path for an older app.

The practical rule is: MMKV for speed, SecureStore for secrets, AsyncStorage for compatibility. MMKV's official README describes fully synchronous calls, multiple instances, encryption support, JSI/NitroModule bindings, and a published benchmark of roughly 30x faster than AsyncStorage. AsyncStorage's current official GitHub docs describe it as asynchronous, unencrypted, persistent key-value storage and note that v3 adds createAsyncStorage scoped storage while preserving the legacy default export. Expo SecureStore's docs describe encrypted local key-value storage backed by platform secure storage on iOS and Android, but warn that large payloads can be rejected and that roughly 2 KB iOS limits have existed historically.

Storage choiceBest useDo not use it for
react-native-mmkvFast reads/writes, render-adjacent preferences, persisted client state, feature flags, offline queuesRaw secrets unless the encryption key is protected outside the JS bundle
AsyncStorageSimple async persistence, old codebases, Expo Go-friendly examples, compatibility-first storagePerformance-critical hydration or sensitive credentials
expo-secure-storeTokens, refresh tokens, API keys, biometric-gated secrets, device-bound keysLarge JSON caches, bulk offline state, hot render-path reads

Key takeaways

  • Choose MMKV for user-visible responsiveness. Synchronous reads are useful when a theme, onboarding flag, cart, or navigation state needs to be available before rendering.
  • Choose SecureStore for credentials. Tokens belong in platform secure storage, not in AsyncStorage and not in a hardcoded MMKV encryption key.
  • Choose AsyncStorage when compatibility beats speed. It is still a useful baseline for simple async storage and incremental migrations, especially when existing libraries already expect its API.
  • Do not treat MMKV encryption as a token-storage replacement by itself. If the key is embedded in JavaScript, the protection is weak. Store the MMKV key in SecureStore if you encrypt larger sensitive data.
  • Keep large values out of SecureStore. Expo explicitly warns that large payloads can be rejected by the underlying platform.
  • Update older AsyncStorage examples carefully. Newer official docs introduce createAsyncStorage("database-name"); the default export remains a backward-compatible singleton for v2-style code.

The fast decision tree

Are you storing auth tokens, refresh tokens, API keys, or private keys?
  → Use expo-secure-store.

Are you storing theme, locale, flags, persisted Zustand/Jotai state, cart state, or small offline queues?
  → Use react-native-mmkv.

Are you in Expo Go, maintaining old AsyncStorage code, or integrating a library that only accepts async storage?
  → Use AsyncStorage, then migrate hot paths to MMKV later.

Are you storing large files, photos, blobs, SQLite-sized data, or hundreds of MB of cache?
  → Use expo-file-system, SQLite, WatermelonDB, Realm, or a real database instead.

Comparison table

FeatureAsyncStoragereact-native-mmkvexpo-secure-store
API shapePromise-based async key-value APISynchronous key-value API plus hooksPromise-based secure key-value API
Official positioningAsynchronous, unencrypted persistent storageFast direct native C++ bindings for React NativeEncrypted local key-value storage
Best fitCompatibility and simple persistencePerformance-sensitive app stateSmall sensitive secrets
Expo supportWorks in Expo projects and Expo Go-style examplesExpo install plus prebuild/development build because it is a native moduleIncluded in Expo Go, with caveats for requireAuthentication
EncryptionNo built-in encryptionMMKV file encryption available; key management is your responsibilityUses platform secure storage on iOS/Android
Multiple storesv3 supports scoped storage via createAsyncStorageMultiple instances via IDsSeparate keys, not multiple high-volume stores
Data typesStrings; serialize objects manuallyStrings, booleans, numbers, buffers; serialize objects manuallyStrings/values intended to stay small
Render-path readsAvoid; async hydration can cause loading statesGood fit; synchronous reads avoid awaitAvoid; secure storage calls are async and slower
Migration difficultyBaseline/legacy optionRequires native setup and call-site updatesRequires async token-access flow and error handling

AsyncStorage: compatibility baseline, not the performance choice

AsyncStorage is still worth knowing because a lot of React Native libraries, examples, and older apps use it. The current official README describes it as an asynchronous, unencrypted, persistent key-value storage solution for React Native. Its API is easy to understand: store strings with setItem, read them with getItem, and serialize objects yourself.

import AsyncStorage from "@react-native-async-storage/async-storage";

await AsyncStorage.setItem("theme", "dark");
await AsyncStorage.setItem("user", JSON.stringify({ id: "123", name: "Alice" }));

const theme = await AsyncStorage.getItem("theme");
const userJson = await AsyncStorage.getItem("user");
const user = userJson ? JSON.parse(userJson) : null;

await AsyncStorage.multiSet([
  ["locale", "en-US"],
  ["notifications", "true"],
]);

const entries = await AsyncStorage.multiGet(["theme", "locale"]);

What's changed in current AsyncStorage docs

Older guides usually show only the default export. Current official GitHub docs also document createAsyncStorage, which creates a named scoped storage instance. That matters for apps that want separate databases per user, workspace, or feature.

import { createAsyncStorage } from "@react-native-async-storage/async-storage";

const userStorage = createAsyncStorage("user-1234");

await userStorage.setItem("preferences", JSON.stringify({ theme: "dark" }));
const raw = await userStorage.getItem("preferences");
const preferences = raw ? JSON.parse(raw) : null;

The default export is still useful for backward-compatible v2-style integrations:

import AsyncStorage from "@react-native-async-storage/async-storage";

await AsyncStorage.getItem("legacy-key");

When AsyncStorage is still the right choice

Use AsyncStorage when:

  • your app already uses it and storage is not on a hot path;
  • you need a simple Promise-based adapter for libraries such as Redux Persist or Zustand's async storage adapter;
  • Expo Go compatibility matters more than startup speed;
  • you want to migrate gradually before introducing a native MMKV dependency.

Do not use AsyncStorage for auth tokens or secrets. The official docs call it unencrypted, and Expo's own data-storage guidance describes AsyncStorage as a good choice for data that does not need encryption, such as preferences or app state.


react-native-mmkv: the default for fast local app state

react-native-mmkv is the performance option. The official README describes fully synchronous calls, no async/await or promises, direct native C++ bindings, encryption support, multiple instances, JSI/NitroModule support, and a benchmark of roughly 30x faster than AsyncStorage.

Current MMKV examples use createMMKV:

import { createMMKV } from "react-native-mmkv";

export const storage = createMMKV();

storage.set("theme", "dark");
storage.set("launchCount", 12);
storage.set("onboardingComplete", true);

const theme = storage.getString("theme");
const launchCount = storage.getNumber("launchCount");
const onboardingComplete = storage.getBoolean("onboardingComplete");

storage.remove("theme");
const keys = storage.getAllKeys();

Namespaced MMKV instances

MMKV is a strong fit when you want to isolate app-wide settings, per-user state, and cache-like data without making every caller think about file paths.

import { createMMKV } from "react-native-mmkv";

export const appStorage = createMMKV({ id: "app-storage" });
export const cacheStorage = createMMKV({ id: "cache-storage" });
export const userStorage = createMMKV({ id: "user-1234-storage" });

cacheStorage.clearAll();

MMKV with Zustand persist

MMKV pairs well with Zustand because the storage adapter can hydrate synchronously. That removes an entire class of "wait for storage before rendering" code in preferences, carts, and feature-flag stores.

import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
import { storage } from "@/lib/storage";

type CartItem = { id: string; quantity: number };

type CartState = {
  items: CartItem[];
  addItem: (item: CartItem) => void;
};

const mmkvStorage = createJSONStorage(() => ({
  setItem: (name: string, value: string) => storage.set(name, value),
  getItem: (name: string) => storage.getString(name) ?? null,
  removeItem: (name: string) => storage.remove(name),
}));

export const useCartStore = create<CartState>()(
  persist(
    (set) => ({
      items: [],
      addItem: (item) => set((state) => ({ items: [...state.items, item] })),
    }),
    {
      name: "cart-storage",
      storage: mmkvStorage,
    },
  ),
);

MMKV encryption and key management

MMKV supports encrypted storage, but encryption is only as strong as the key-management pattern. If the encryption key is hardcoded in the JavaScript bundle, a motivated attacker can extract it. A safer pattern is to generate a random key, keep that key in SecureStore, and use it to initialize or encrypt MMKV.

import * as SecureStore from "expo-secure-store";
import { createMMKV } from "react-native-mmkv";

const MMKV_KEY = "mmkv-encryption-key";

async function getOrCreateEncryptionKey() {
  const existing = await SecureStore.getItemAsync(MMKV_KEY);
  if (existing) return existing;

  const key = generateRandomKey(64); // Use expo-random, react-native-get-random-values, or your approved crypto helper.
  await SecureStore.setItemAsync(MMKV_KEY, key);
  return key;
}

export async function createEncryptedStorage() {
  const encryptionKey = await getOrCreateEncryptionKey();

  return createMMKV({
    id: "encrypted-app-storage",
    encryptionKey,
    encryptionType: "AES-256",
  });
}

Use this for larger sensitive local datasets where SecureStore is too small or too slow. For normal auth tokens, SecureStore alone is simpler and easier to audit.


expo-secure-store: the credential store

Expo SecureStore is the right default for auth tokens, refresh tokens, API keys, private keys, and other small values that should not be stored in plaintext. The Expo docs describe it as a way to encrypt and securely store key-value pairs locally on the device. On Android, values are stored in SharedPreferences encrypted with Android's Keystore system. On iOS, values are stored using Keychain services as kSecClassGenericPassword.

import * as SecureStore from "expo-secure-store";

await SecureStore.setItemAsync("access_token", accessToken);
await SecureStore.setItemAsync("refresh_token", refreshToken);

const token = await SecureStore.getItemAsync("access_token");

await SecureStore.deleteItemAsync("access_token");
await SecureStore.deleteItemAsync("refresh_token");

SecureStore options worth caring about

await SecureStore.setItemAsync("refresh_token", refreshToken, {
  keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
});

await SecureStore.setItemAsync("private_key", privateKey, {
  requireAuthentication: true,
  authenticationPrompt: "Unlock your key",
});

Use WHEN_UNLOCKED_THIS_DEVICE_ONLY when a credential should stay tied to the current device rather than moving through backup/restore flows. Use requireAuthentication only after testing the actual Expo/client environment because Expo documents an Expo Go caveat for biometric authentication.

SecureStore is not a cache

SecureStore is for small secrets, not large offline payloads. Expo warns that large payloads can be rejected by the underlying platform and notes that some iOS releases historically refused values above roughly 2048 bytes. If you need to persist a large object securely, store a SecureStore-protected encryption key and put the encrypted payload in MMKV, SQLite, or a file/database layer designed for larger values.


What to store where

Access token
  → expo-secure-store

Refresh token
  → expo-secure-store, often with WHEN_UNLOCKED_THIS_DEVICE_ONLY

MMKV encryption key
  → expo-secure-store

Theme, locale, onboarding flag
  → react-native-mmkv

Persisted Zustand/Jotai state
  → react-native-mmkv

Feature flags and remote-config cache
  → react-native-mmkv

Small API response cache
  → react-native-mmkv if read often; AsyncStorage if compatibility matters

Draft form state
  → react-native-mmkv

Large files, images, SQLite-like data, document caches
  → expo-file-system, SQLite, Realm, WatermelonDB, or another data store

Migration strategy: AsyncStorage to MMKV

The safest production migration is a one-time copy plus a completion flag. Do not replace every await AsyncStorage.getItem call blindly and hope the app behaves the same. AsyncStorage is async; MMKV is sync. Hydration timing changes, which is usually a benefit but can expose assumptions in startup code.

import AsyncStorage from "@react-native-async-storage/async-storage";
import { createMMKV } from "react-native-mmkv";

const storage = createMMKV();
const MIGRATED_KEY = "async-storage-migrated-v1";

export async function migrateAsyncStorageToMMKV() {
  if (storage.getBoolean(MIGRATED_KEY)) return;

  const keys = await AsyncStorage.getAllKeys();
  const entries = await AsyncStorage.multiGet(keys);

  for (const [key, value] of entries) {
    if (value !== null) {
      storage.set(key, value);
    }
  }

  storage.set(MIGRATED_KEY, true);
}

Run the migration early in app startup, then update call sites deliberately:

// Before
const theme = await AsyncStorage.getItem("theme");

// After
const theme = storage.getString("theme") ?? "system";

Keep a rollback window. For the first release, copy values into MMKV and leave AsyncStorage intact. After production telemetry shows the migration is stable, ship a second release that removes the old AsyncStorage writes and clears legacy keys.

Security model: what "secure" means here

AsyncStorage is unencrypted. It is fine for non-sensitive preferences and app state, but not for credentials, tokens, or regulated personal data.

MMKV can encrypt its storage file. That protects the file at rest, but only if the key is protected. A hardcoded JavaScript key is not enough. A SecureStore-backed key is a stronger pattern for larger encrypted local datasets.

SecureStore delegates to platform secure storage. That is the best fit for secrets, but it comes with async access, platform-specific behavior, and size limits. Design token access around startup/session boundaries instead of calling SecureStore repeatedly during render.

React Native New Architecture implications

The New Architecture strengthens the MMKV case because MMKV is built around direct native bindings rather than the old serialized bridge pattern. Its synchronous reads are most valuable for values needed immediately: theme, language, feature flags, session hints, and persisted UI state.

AsyncStorage remains async even when the app runs on newer React Native architecture. That does not make it bad; it means you should keep it away from hot render paths. SecureStore is async by design because secure platform storage involves OS-level calls and optional authentication.

Source notes

Sources checked on 2026-05-15:

  • react-native-mmkv official GitHub README: https://github.com/mrousavy/react-native-mmkv
  • react-native-mmkv npm registry metadata: https://registry.npmjs.org/react-native-mmkv
  • AsyncStorage official GitHub README and docs source: https://github.com/react-native-async-storage/async-storage, especially docs/api/usage.md and docs/faq.md
  • AsyncStorage npm registry metadata: https://registry.npmjs.org/@react-native-async-storage%2fasync-storage
  • Expo SecureStore docs: https://docs.expo.dev/versions/latest/sdk/securestore/
  • Expo data-storage guidance: https://docs.expo.dev/develop/user-interface/store-data/

One older AsyncStorage public docs path (react-native-async-storage.github.io/async-storage/) returned 404 during refresh, so this guide cites the maintained GitHub source/docs and package registry metadata instead of relying on that stale URL.


Related: Zustand vs Jotai vs Nano Stores for the state management layer that often persists into MMKV or AsyncStorage, Gluestack UI vs React Native Paper vs Unistyles for React Native UI stack decisions, and NativeWind vs Tamagui vs twrnc for styling choices in the same app architecture.

See also: React vs Vue and React vs Svelte.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.