Shipnative
ShipnativeShipnative
Sign in
GuideAugust 2026 · 9 min read

React Native MMKV: Storage Without the Await

MMKV is usually sold on speed, and the benchmarks are real, but speed is rarely why it earns its place in a project. The reason is the missing await. A synchronous read can happen inside a render, a store initialiser, or the first line of a component — which deletes the whole category of bugs where an app flashes the wrong theme, the logged-out screen, or an empty list for 200 milliseconds on every cold start. The cost is that it ships native code, so Expo Go is off the table. Here is the honest version of that trade.

What it actually is

MMKV is a key-value store written in C++ at Tencent, used in WeChat, and wrapped for React Native by Marc Rousavy. The wrapper installs through JSI rather than the old bridge, so JavaScript holds a direct reference to the native object. That is the entire technical story, and it explains every practical difference: no serialisation across a bridge, no promises, no queue.

It is still a key-value store. It is not a database, it does not query, and it is not the place for a thousand-row list — that is what expo-sqlite is for. Think of it as AsyncStorage with the await removed and a few extra types, and you will use it correctly.

Install, and the build you now need

npx expo install react-native-mmkv

# then a native build — MMKV is not in Expo Go
npx expo run:ios
npx expo run:android

# or a cloud build for teammates and devices you do not have
eas build --profile development --platform all

If you install it and keep running in Expo Go, you get a runtime error along the lines of the MMKV native module not being available — not a build failure, so it is easy to misread as a configuration problem. It is not: the binary simply does not contain the library. A development build is a one-time cost and unlocks every other native module too, but it is a real decision, especially early in a project when Expo Go is doing a lot of work for you.

One instance, one typed module

// lib/storage.ts
import { MMKV } from 'react-native-mmkv';

export const storage = new MMKV({ id: 'app' });

// A separate file per concern keeps a cache wipe from touching settings.
export const cache = new MMKV({ id: 'cache' });

export const KEYS = {
  theme: 'settings.theme.v1',
  onboarded: 'settings.onboarded.v1',
  draft: 'compose.draft.v1',
} as const;

export function getJSON<T>(key: string, fallback: T): T {
  const raw = storage.getString(key);
  if (raw == null) return fallback;
  try {
    return JSON.parse(raw) as T;
  } catch {
    storage.delete(key);          // a corrupt value is a cache miss, not a crash
    return fallback;
  }
}

export function setJSON(key: string, value: unknown) {
  storage.set(key, JSON.stringify(value));
}

Primitives skip the JSON round-trip entirely, which is the small daily pleasure of using MMKV:

storage.set(KEYS.onboarded, true);
storage.set('launchCount', (storage.getNumber('launchCount') ?? 0) + 1);

const theme = storage.getString(KEYS.theme) ?? 'system';   // no await, usable in render
const seen  = storage.getBoolean(KEYS.onboarded) ?? false;

storage.delete(KEYS.draft);
storage.getAllKeys();               // returns synchronously too

Named instances are worth setting up on day one. A single namespace means your “clear cache” button either wipes user settings too or grows a hand-maintained list of keys to spare. Two instances make it one call.

The bug this actually fixes

Compare the two shapes side by side. With AsyncStorage, the value arrives after the first render, so you hold a flag and gate the tree:

// AsyncStorage: renders once with the wrong value, then corrects
const [theme, setTheme] = useState('system');
const [hydrated, setHydrated] = useState(false);
useEffect(() => {
  AsyncStorage.getItem('theme').then((t) => { setTheme(t ?? 'system'); setHydrated(true); });
}, []);
if (!hydrated) return null;

// MMKV: correct on the very first frame
const [theme, setTheme] = useState(() => storage.getString('theme') ?? 'system');

The library also ships reactive hooks, which re-render on write from anywhere in the app — including another instance of the same key set in a completely different screen:

import { useMMKVString, useMMKVBoolean } from 'react-native-mmkv';

function ThemeToggle() {
  const [theme, setTheme] = useMMKVString(KEYS.theme, storage);
  return <Switch value={theme === 'dark'} onValueChange={(v) => setTheme(v ? 'dark' : 'light')} />;
}

That is genuinely convenient for small global settings and genuinely a trap for anything bigger. Storage is not a state manager: putting a form draft or a selected filter behind these hooks means every keystroke hits disk and every consumer re-renders. Keep the hooks for values that change a few times a session.

Migrating off AsyncStorage

// Run once, before the first render that reads storage.
const MIGRATED = 'migrated.mmkv.v1';

export async function migrateFromAsyncStorage() {
  if (storage.getBoolean(MIGRATED)) return;

  const keys = await AsyncStorage.getAllKeys();
  const pairs = await AsyncStorage.multiGet(keys);
  for (const [key, value] of pairs) {
    if (value != null) storage.set(key, value);   // values are already strings
  }

  storage.set(MIGRATED, true);
  // Leave the AsyncStorage copy in place for one release, in case you roll back.
}

Two cautions. Do not delete the AsyncStorage data in the same release — if you ship a fix that reverts the change, or a user rolls back through an over-the-air update, the old code path needs something to read. And be aware that libraries you did not write may still hold their own AsyncStorage keys; migrating the whole namespace copies them, but those libraries keep reading from the original store regardless.

MMKV against AsyncStorage, honestly

MMKVAsyncStorage
APISynchronous, returns valuesPromise-based, needs await
Expo GoNot supported — needs a dev buildWorks as-is
Read during renderYesNo — causes the hydration flash
Typesstring, number, boolean, Uint8ArrayStrings only, so JSON everywhere
EncryptionOptional, with a key you must store safelyNone
Size ceilingNo fixed cap, meant for small values6 MB default on Android
Multiple storesNamed instances with separate filesOne shared namespace

The recommendation that survives contact with real projects: start on AsyncStorage, because Expo Go is worth a lot in the first weeks and most apps read half a dozen keys at launch. Move to MMKV when you have already left Expo Go for another reason, or when you are actively fighting hydration ordering. Moving for benchmark numbers alone, in an app that stores a theme and an onboarding flag, is optimising something no user will ever perceive.

Skip the plumbing

Storage module, key registry, migration guard — the same afternoon in every project, before you write a feature. Describe your app at shipnative.dev and it generates a React Native app with persistence already wired correctly, running on your phone in minutes, with the full Expo project available to export.

Frequently Asked Questions

Does react-native-mmkv work in Expo Go?

No. It ships native code and is not part of the Expo Go binary, so you need a development build (npx expo run:ios, npx expo run:android, or an EAS build). This is the single biggest practical difference from AsyncStorage, which runs in Expo Go unchanged. No config plugin or extra setup is needed beyond the build itself.

Why is MMKV synchronous when AsyncStorage is not?

AsyncStorage returns promises because its calls historically crossed the asynchronous bridge. MMKV is installed through JSI, which gives JavaScript a direct handle on the C++ object, so a read is an ordinary function call that returns a value. That is why you can read a stored theme during the first render instead of after it.

Is MMKV faster than AsyncStorage?

Yes, and the library publishes benchmarks showing a large multiple. The number matters less than the shape: because reads are synchronous, an MMKV read can happen inside a render or a Zustand initialiser, which removes an entire class of hydration bugs rather than just making an existing call quicker. If your app reads a handful of keys at launch, the speed difference alone will not be visible to a user.

Is MMKV encrypted?

Only if you pass an encryptionKey when constructing the instance, and even then the key has to be stored somewhere — which means expo-secure-store or the Keychain, not a constant in your bundle. For a small number of secrets, keeping them in expo-secure-store directly is simpler and uses the OS keystore. MMKV encryption is for bulk data you want unreadable at rest, not as a Keychain replacement.

Can I migrate from AsyncStorage without losing data?

Yes. Read every AsyncStorage key once on first launch after the upgrade, write them into MMKV, and record a migration flag. Because MMKV is synchronous while AsyncStorage is not, the migration itself is the only asynchronous step, and after it completes the rest of your app can read values inline.

Does it work with Zustand or Redux Persist?

Yes. Both accept a custom storage adapter, and MMKV needs a three-method shim exposing getItem, setItem, and removeItem. With Zustand the payoff is real: a synchronous adapter means the store is already hydrated on the first render, so hasHydrated gating becomes unnecessary.

→

React Native AsyncStorage

The default, its ceilings, and the hydration flash MMKV removes.

Read guide →
→

Expo Go vs Development Build

What you give up and gain by leaving Expo Go.

Read guide →

Ship a real React Native app today

Describe, preview, and export Expo code — free to start.

Build with ShipNative →
ShipNative logoShipnative

Build mobile apps with AI. Describe, preview, and ship to iOS & Android in minutes.

Features

Text to App AIApp Generator from ScreenshotPRD to Mobile App

Tools

All free toolsApp Cost CalculatorApp Name GeneratorApp Store Keyword ToolReact Native Components

Blog

All blog postsHow to Build an App Without CodingBest AI Tools for Real Mobile AppsExpo EAS App Store ChecklistLovable, Cursor & v0 for MobileBest AI App Builders in 2026React Native AI App Builder Guide

Legal

FAQTerms of ServicePrivacy Policy

© 2026 ShipNative. All rights reserved.