Skip to main content

react-native-mmkv vs AsyncStorage vs expo-secure-store 2026

·PkgPulse Team

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 readsstorage.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

FeatureAsyncStorageMMKVexpo-secure-store
Read performanceSlow (async)✅ Synchronous, fastSlow (async)
Encryption✅ Optional AES✅ Hardware (Keychain/Keystore)
Synchronous API
Size limit (Android)6MB defaultNo limit2KB per key
Expo Go
New Architecture
Multiple instances
Biometric lock
Binary data
GitHub stars4.5k8.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.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.