Back to BlogReact Native Development

React Native MMKV Complete Guide to Fast Local Storage

Mark Louis
Mark LouisSeptember 10, 2026
React Native MMKV

A practical, production-focused guide to React Native MMKV covering its Nitro Modules architecture, current installation steps, Zustand and Redux Persist integration, encryption limits, storage size management, Jest testing, and a safe migration path off AsyncStorage.

If you've profiled a React Native app development and found AsyncStorage calls stacking up on startup or during frequent writes, you've probably already looked at React Native MMKV. It's become the default local storage choice for a large share of production apps, and for good reason: it swaps async bridge calls for synchronous, native-speed reads and writes. This guide covers what MMKV actually is, how it works under the hood, how to install and use the current version, and where it fits, and doesn't fit, in a real app's storage strategy.

What Is React Native MMKV?

MMKV is a key-value storage engine originally built by We-chat's software engineering team in C++ and open-sourced as Tencent/MMKV, designed for fast, small, reliable local storage on mobile. react-native-mmkv, the binding that exposes it to JavaScript, is maintained by Marc Rousavy and the Margelo team, wrapping the native engine with a typed JS/Typescript API built specifically for React Native.

Unlike AsyncStorage, which was built as a simple async key-value wrapper, MMKV was designed from the start around memory-mapped files and synchronous native calls. It stores strings, numbers, boolean, and raw Array Buffers directly, with built-in encryption and support for multiple isolated storage instances in the same app.

How Does MMKV Work Under the Hood?

JSI and Nitro Modules

The current major version, react-native-mmkv v4, is built on Nitro Modules, a code-generation layer that produces type-safe native bindings directly from TypeScript definitions. Nitro sits on top of JSI, React Native's low-level JavaScript-to-native interface, so calls reach the C++ MMKV engine without crossing the old asynchronous bridge or JSON-serializing every payload.

Memory-Mapped Files

MMKV persists data using memory-mapped files (mmap). Instead of opening a file, reading it into memory, and writing it back on every operation, the OS maps the file directly into the app's memory space. Reads become simple memory reads, and writes are flushed by the OS's own paging mechanism.

Why Synchronous Calls Are Safe, and When They Aren't

Because reads and writes resolve in the same tick, they're fast enough to run synchronously on the JS thread without noticeably blocking it, for typical small key-value payloads on a Hermes-powered app. That caveat matters: synchronous calls stop being harmless if you use MMKV for large blobs, or if you fire many writes in a tight loop on the JS thread during an animation or gesture. For those cases, batch writes or move heavy serialization off the hot path rather than assuming every synchronous call is free.

MMKV vs AsyncStorage

Both solve the same basic problem, persisting simple values between app launches, but they differ in how they get there and what they optimize for.

Aspect

MMKV

AsyncStorage

Underlying engine

C++ (Tencent MMKV), memory-mapped files

SQLite (Android) / files (iOS), via a native module

Call style

Fully synchronous

Asynchronous, Promise-based

Native communication

Nitro Modules / JSI, no bridge serialization

Native module calls, historically bridge-based

Typed getters

getString / getNumber / getBoolean / getBuffer

getItem returns strings only; you parse the rest

Encryption

Built in (AES-128 / AES-256)

None; needs a separate secure-storage layer

Multiple instances

Yes, by storage ID

Single default store per app

Best fit

Frequent reads/writes, app state, preferences, feature flags

Simple, infrequent writes where async fits the flow naturally

Read/Write Latency and Bridge Overhead

AsyncStorage serializes every call into a message, sends it across the bridge or native module boundary, and waits for a Promise to resolve, even for a single cached string. MMKV skips that entirely: a getString or set call executes synchronously in native code and returns in the same tick. In practice, this removes the async overhead of state hydration on app boot and eliminates a class of race conditions that come from juggling Promises during rapid sequential writes.

How to Install MMKV in React Native

As of v4, react-native-mmkv is a Nitro Module, which means it has a peer dependency on react-native-nitro-modules and requires React Native 0.76 or higher. Teams on older React Native versions should use the separately documented v3 release line instead of forcing v4.

Bare React Native Workflow

npm install react-native-mmkv react-native-nitro-modules
cd ios && pod install

Android requires no manual linking beyond a standard rebuild.

Expo (Development Build Required)

MMKV relies on native code, so it will not run inside Expo Go. You need a development build.

npx expo install react-native-mmkv react-native-nitro-modules
npx expo prebuild
After prebuild, run the app with a dev client (npx expo run:ios / run:android) rather than the Expo Go app.

How to Store and Retrieve Data with MMKV

Create one shared instance and export it, rather than instantiating MMKV repeatedly.

import { createMMKV } from 'react-native-mmkv'
 
export const storage = createMMKV()

This factory function is the current API. Older tutorials and even some still-circulating documentation use a new MMKV() constructor from v2/v3; that syntax still appears widely online but is not the current recommended pattern.

Strings, Numbers, Booleans

storage.set('username', 'jordan_dev')
storage.set('sessionCount', 12)
storage.set('onboardingComplete', true)
 
const username = storage.getString('username')
const sessionCount = storage.getNumber('sessionCount')
const onboardingComplete = storage.getBoolean('onboardingComplete')

Objects and JSON

const prefs = { theme: 'dark', locale: 'en-US' }
storage.set('userPrefs', JSON.stringify(prefs))
 
const raw = storage.getString('userPrefs')
const parsedPrefs = raw ? JSON.parse(raw) : null

ArrayBuffers

MMKV can also store raw binary data directly, which is useful for tokens or small binary payloads that don't need JSON serialization.

const buffer = new ArrayBuffer(3)
const writer = new Uint8Array(buffer)
writer[0] = 1; writer[1] = 100; writer[2] = 255
storage.set('someToken', buffer)
 
const stored = storage.getBuffer('someToken')

Keys: Checking, Listing, Deleting, Clearing

storage.contains('username')      // boolean
storage.getAllKeys()               // string[]
storage.remove('sessionCount')     // delete one key
storage.clearAll()                 // wipe this instance

Using MMKV with React Hooks

The Hooks API keeps components in sync with storage changes without manual listeners.

import { useMMKVString, useMMKVNumber, useMMKVBoolean } from 'react-native-mmkv'
 
function ProfileScreen() {
  const [username, setUsername] = useMMKVString('username')
  const [sessionCount] = useMMKVNumber('sessionCount')
 
  return null // render UI using username / sessionCount
}

Each hook subscribes only to its own key, so updates to unrelated keys won't trigger a re-render, which matters in screens reading several values at once.

Multiple Instances and Advanced Storage Scenarios

Separating Global and Per-User Storage

By using a custom storage ID, you can keep global app data and a logged-in user's data in separate MMKV instances, which makes logout and account-switching cleanup straightforward.

export const appStorage = createMMKV({ id: 'app-global' })
export const userStorage = createMMKV({ id: user-${userId} })

App Groups, Extensions, and Multi-Process Mode

If your app shares data with an iOS widget, share extension, or another app in the same App Group, MMKV supports this through its multi-process mode. Setting mode to multi-process tells the MMKV instance to assume data can change from outside the current process, which keeps reads consistent when a widget and the main app write to the same storage.

export const sharedStorage = createMMKV({
  id: 'widget-shared',
  mode: 'multi-process',
})

MMKV with Zustand and Redux Persist

MMKV works well as the persistence layer under a state manager, since the library's own docs ship official adapters for both.

Zustand Persist Middleware

import { createMMKV } from 'react-native-mmkv'
import { StateStorage } from 'zustand/middleware'
 
const storage = createMMKV()
 
export const zustandStorage: StateStorage = {
  setItem: (name, value) => storage.set(name, value),
  getItem: (name) => storage.getString(name) ?? null,
  removeItem: (name) => storage.remove(name),
}

Pass zustandStorage into Zustand's persist middleware as the storage option, and Zustand handles serialization on top of MMKV's synchronous set/get calls.

Redux Persist

The same pattern applies with redux-persist: implement a storage object exposing set-item, get-item, and remove-item backed by an MMKV instance, and pass it as the storage engine in your persist-config. Because the underlying calls are synchronous, re-hydration on app start is effectively instant compared to an AsyncStorage-backed store.

MMKV Encryption and Security

MMKV supports encryption at rest using AES-128 by default, with AES-256 available as an option.

export const secureStorage = createMMKV({
  id: 'secure-storage',
  encryptionKey: 'a-strong-random-key',
  encryptionType: 'AES-256',
})

This is genuinely useful for reducing exposure if a device's file-system is inspected outside the OS sandbox, but it has real limits worth stating plainly. The encryption key itself has to live somewhere, and if you hard-code it in JavaScript or store it in another unencrypted MMKV key, you haven't meaningfully protected anything. MMKV's encryption is not a substitute for OS-level secure storage. Authentication tokens, refresh tokens, and other high-value secrets belong in the iOS Keychain or Android Keystore, accessed through expo-secure-store or react-native-key-chain, where the OS manages key material in hardware-backed storage. Use MMKV encryption for reducing casual exposure of app data, not as the sole defense for credentials.

MMKV Performance and Benchmarks

MMKV is faster than AsyncStorage primarily because it avoids two costs: bridge/Promise overhead on every call, and repeated file I/O for small reads. Memory-mapped access means a read is close to a direct memory access once the file is mapped, and synchronous calls remove the micro-task and native-module round trip that async storage requires even for a single cached value. This also shortens state hydration on app launch, since values can be read before the first render instead of waiting on a Promise chain.

The library's own README cites a roughly 30x improvement over AsyncStorage on a 1,000-read benchmark, based on the maintainer's published Storage-benchmark project, run on a specific device. That number is directional evidence the architecture is faster, not a guarantee for every app. Real-world gains depend on device tier, how many keys a given screen reads, React Native and Hermes versions, and whether storage is actually your bottleneck versus rendering or network.

MMKV and React Native New Architecture

react-native-mmkv v4 is built on Nitro Modules rather than the classic bridge. Nitro is a distinct code-generation layer from Turbo-modules, though both sit on top of JSI for synchronous native calls, so it's not accurate to describe current MMKV internals as "a Turbo-module." In practice, v4 is built for New Architecture-era React Native rather than bolted onto it, and it requires React Native 0.76 or higher. Teams on older React Native versions or still fully on the legacy bridge should stay on the v3 line documented separately in the project's repository until they upgrade.

Storage Size Management in Production

MMKV is efficient, but long-lived user sessions can still accumulate unused keys over time, particularly in apps that cache a lot of transient data. The library exposes tools to monitor and reclaim space.

const size = storage.byteSize
if (size >= 4096) {
  storage.trim() // clean unused keys and clear memory cache
}

Cleanup on Logout and App Reset

Because MMKV supports multiple instances, a clean logout pattern is to clear only the per-user instance rather than the global app instance, so preferences like theme or locale survive a logout while session-specific data doesn't.

userStorage.clearAll()   // wipe this user's data on logout
// appStorage is untouched, so app-level settings persist

Migrating from AsyncStorage to MMKV

  • Audit existing AsyncStorage keys: list every key currently in use and its expected data shape (string, JSON blob, boolean flag).

  • Map data types: decide which keys map to get-string/get-number/get-Boolean versus JSON-serialized objects, since MMKV has native typed getters AsyncStorage doesn't.

  • Create the MMKV storage layer: stand up a single shared createMMKV() instance, or a small wrapper module, that the rest of the app imports instead of calling AsyncStorage directly.

  • Migrate existing values: on app start, read each AsyncStorage key, write it into MMKV under the same key, and only then remove it from AsyncStorage, run once behind a migration-complete flag.

  • Validate data parity: compare a sample of migrated values across both stores in development/staging builds before shipping.

  • Test production flows end-to-end: on-boarding, login, and any screen reading storage on cold start, where race conditions are most likely to surface.

  • Remove AsyncStorage only after validation: keep it installed and untouched until the migration is confirmed across your real user base, then remove the dependency in a later release.

Post-Migration Validation Checklist

  • Diff default-value behavior: AsyncStorage's getItem resolves to null for a missing key, while MMKV's typed getters resolve to undefined. Any code branching on a falsy default needs re-checking.

  • Run a full getAllKeys() pass on both stores after migration to catch orphaned entries left behind in AsyncStorage.

  • Spot-check logout and app-reset flows specifically, since these are the flows most likely to reveal a key that was missed in the audit step.

Testing MMKV with Jest

MMKV is a native module, so Jest can't execute its underlying C++ code in a Node environment. The current version handles this automatically: a mocked MMKV instance is used when running under Jest or Vitest, so createMMKV() works in tests without manual jest.mock setup in most cases.

import { createMMKV } from 'react-native-mmkv'
 
test('stores and retrieves a value', () => {
  const storage = createMMKV()
  storage.set('key', 'value')
  expect(storage.getString('key')).toBe('value')
})

If your test setup still needs explicit control, such as a custom Jest config or a monorepo with non-standard module resolution, fall back to a manual mock module for react-native-mmkv that implements the same method surface backed by a plain in-memory object.

Common MMKV Errors and Troubleshooting

  • "Cannot find native module" after install: usually means pod install wasn't run on iOS, or the native build wasn't rebuilt after adding the dependency. A full clean rebuild resolves most of these.

  • Crashes or hangs in Expo Go: MMKV requires native code and cannot run inside Expo Go; you need a development build via expo prebuild.

  • Remote JS debugging (Chrome) stops working: expected behaviour. Because MMKV uses synchronous native calls, the classic remote debugger, which runs JS off-device, isn't compatible. Use Flipper (with the MMKV plugin), Rozenite, or React DevTools instead. Reactotron can also log writes automatically via its MMKV plugin.

  • Peer dependency errors mentioning react-native-nitro-modules: v4 requires this package explicitly; install it alongside react-native-mmkv rather than relying on it being pulled in transitively.

  • Build failures on React Native versions below 0.76: v4 has a hard minimum version requirement; teams on older React Native should use the v3 release line instead of forcing v4.

  • Jest tests failing to find a native module: confirm you're on a version where auto-mocking applies, or add an explicit manual mock for the package in your Jest config.

When Should You Use MMKV?

  • App state and UI preferences read on nearly every screen (theme, locale, layout flags)

  • Feature flags and remote-config caches that need to be available synchronously before first render

  • Session-level data and lightweight caching where read/write frequency is high

  • Backing store for Zustand, Redux Persist, or similar state libraries in apps with frequent state updates

  • Sharing small amounts of data with a widget or extension via App Groups and multi-process mode

When Should You NOT Use MMKV?

  • High-value secrets (auth/refresh tokens, API keys); use Keychain/Keystore via expo-secure-store or react-native-keychain instead

  • Large, relational, or queryable datasets; reach for SQLite (expo-SQLite, op-SQLite) or Watermelon-db

  • Apps still running on Expo Go for rapid prototyping, where native modules aren't available at all

  • Projects on React Native versions below 0.76 that aren't ready to upgrade, where the v3 line is the more realistic near-term option

MMKV vs AsyncStorage vs SecureStore

Rather than picking one storage engine for an entire app, most production codebases end up using two or three side by side, matched to what's actually being stored.

Data type

Recommended storage

UI state, theme, feature flags, cache

MMKV

Auth tokens, refresh tokens, API keys

Keychain (iOS) / Keystore (Android) via expo-secure-store or react-native-keychain

Large relational or queryable datasets

SQLite (expo-sqlite, op-sqlite) or WatermelonDB

Legacy code with low write frequency, Expo Go

AsyncStorage, migrate later if it becomes a bottleneck

Frequently Asked Questions

What is MMKV in React Native?

It's a fast, synchronous key-value storage library for React Native, built on the C++ MMKV engine originally developed by WeChat and exposed to JavaScript through react-native-mmkv.

Is MMKV better than AsyncStorage?

For most new apps, yes, particularly for frequent reads and writes and for synchronous access on app start. AsyncStorage still works fine for simple, infrequent, non-blocking use cases and remains more compatible with Expo Go.

Is MMKV compatible with Expo?

Yes, but only with a development build via expo prebuild. It does not run inside Expo Go because it requires native code.

Does MMKV work with the React Native New Architecture?

Yes. Version 4 is built on Nitro Modules, which sit on the same JSI foundation as the New Architecture's Turbo-modules, and it requires React Native 0.76 or higher.

Is MMKV secure?

It supports AES-128/AES-256 encryption at rest, which helps against casual file inspection, but it is not a replacement for OS-level secure storage. High-value secrets should still go in the Keychain or Keystore.

Let's Build Something Extraordinary

Turn ideas into intelligent products that drive real business results.