Every React Native developer eventually hits the same wall: you need to read a single, tiny piece of state, an auth token, a theme preference, an onboarding flag, and the moment you reach for it, your code sprouts an await. The default tool, AsyncStorage, is Promise-based, which means even the most trivial lookup becomes asynchronous, forcing you to thread loading states through code that should be dead simple. MMKV (react-native-mmkv) throws that constraint out the window.
MMKV is a synchronous, typed, optionally encrypted key-value storage framework for React Native. It wraps the native C++ core that Tencent built and battle-tested inside WeChat, then exposes it to JavaScript through fast, bridgeless bindings. Reading a value is instant: no Promises, no await, just a function call that returns the answer. It is the natural persistence layer for app settings, feature flags, session tokens, cached API responses, and the storage backend behind state libraries like Redux Persist, Zustand, and Jotai. If you have ever wanted your storage to feel like a plain JavaScript object that happens to survive app restarts, this is it.
Why It Feels Like Cheating
MMKV earns its tagline, "the fastest key-value storage for React Native," through a handful of deliberate engineering choices that compound into a dramatically better developer experience.
- Synchronous by design.
storage.getString('theme')returns the value immediately. No async ceremony, no loading spinners for trivial reads, and you can call it inside reducers, selectors, or right before your first render. - Genuinely fast. It reads and writes through JSI rather than the old React Native bridge, so there is no serialization tax crossing the JavaScript-to-native boundary. The README claims roughly 30x faster than AsyncStorage on a 1000-read benchmark.
- Properly typed. Strings, numbers, booleans, and
ArrayBufferbinary data are all first-class. You are not forced to stringify everything into JSON. - Encrypted at rest. Optional AES-128 or AES-256 encryption protects sensitive values without bolting on a separate library.
- Reactive hooks included. Hooks like
useMMKVStringsubscribe components to keys and re-render on change, turning MMKV into a tiny global store with zero providers.
Under the hood, the namesake gives the trick away: MMKV uses memory-mapped IO (mmap) plus lazy writes on top of a proven native core, accessed synchronously over JSI. The current major line, V4, is built as a Nitro Module, the author's high-performance native module framework, which is why it is so quick.
A Note on V4 and the Nitro Shift
Before the code, one important heads-up. There are two API generations you will run into in the wild, and tutorials are split between them.
- V3 and earlier treated
MMKVas a JavaScript class. You created instances withnew MMKV(). The vast majority of older blog posts use this. - V4 (current, 4.x) rebuilt MMKV as a Nitro Module. Instances are now pure native objects created through a factory function,
createMMKV(), and you must install thereact-native-nitro-modulescore dependency.
The mental model is identical in both: a synchronous, typed key-value store. Only the constructor and a couple of method names changed (notably delete() became remove(), since delete is a reserved word in C++). This article uses the modern V4 API throughout.
Getting It Onto Your Device
For a bare React Native project, install MMKV alongside the Nitro modules core, then run pod install for iOS.
npm install react-native-mmkv react-native-nitro-modules
cd ios && pod install
Prefer yarn? It is just as straightforward.
yarn add react-native-mmkv react-native-nitro-modules
cd ios && pod install
For Expo, use expo install so versions line up, then create a development build because MMKV needs native code and will not run inside Expo Go.
npx expo install react-native-mmkv react-native-nitro-modules
npx expo prebuild
A couple of requirements to keep in mind: V4 expects React Native 0.76 or higher, and because it relies on JSI you cannot use Chrome's remote JavaScript debugging. Reach for Flipper, React DevTools, or the Hermes debugger instead.
Storing and Reading Your First Values
Create an instance once and export it from a module so the rest of your app can import the same store. The default createMMKV() call gives you a sensible shared instance.
// storage.ts
import { createMMKV } from 'react-native-mmkv'
export const storage = createMMKV()
Now writing and reading is as direct as it gets. Each type has its own setter overload and a matching typed getter, so TypeScript knows exactly what comes back.
import { storage } from './storage'
// Writing typed values
storage.set('user.name', 'Marc')
storage.set('user.age', 21)
storage.set('onboarding.complete', true)
// Reading them back, synchronously
const name = storage.getString('user.name') // string | undefined
const age = storage.getNumber('user.age') // number | undefined
const done = storage.getBoolean('onboarding.complete') // boolean | undefined
Notice there is no await anywhere. The getters return undefined when a key is missing, which makes them safe to call at startup without guarding against pending Promises. Need to check existence, list keys, or clean up? The housekeeping API is equally plain.
const exists = storage.contains('user.name') // boolean
const keys = storage.getAllKeys() // string[]
storage.remove('user.name') // V4 rename of delete()
storage.clearAll() // wipe the whole instance
Objects, Binary Data, and Reactive Hooks
MMKV stores primitives natively, so for objects you serialize to JSON the usual way. It is a tiny bit of boilerplate, but it keeps the storage layer honest and predictable.
const user = { username: 'Marc', age: 21 }
storage.set('user', JSON.stringify(user))
const json = storage.getString('user')
const restored = json ? JSON.parse(json) : undefined
For binary payloads such as raw tokens or small encoded blobs, MMKV speaks ArrayBuffer directly, no base64 round-trips required.
const buffer = new ArrayBuffer(3)
const writer = new Uint8Array(buffer)
writer[0] = 1
writer[1] = 100
writer[2] = 255
storage.set('someToken', buffer)
const out = storage.getBuffer('someToken') // Uint8Array [1, 100, 255]
Where MMKV really shines for UI code is its hooks. They subscribe a component to a specific key and re-render it whenever that key changes, anywhere in the app. That effectively turns MMKV into a minimal reactive global store with no Context or provider in sight.
import {
useMMKVString,
useMMKVNumber,
useMMKVBoolean,
useMMKVObject,
} from 'react-native-mmkv'
function Profile() {
const [name, setName] = useMMKVString('user.name')
const [age, setAge] = useMMKVNumber('user.age')
const [isDark, setIsDark] = useMMKVBoolean('theme.dark')
const [user, setUser] = useMMKVObject<{ username: string }>('user')
// Setters accept a value or an updater. Passing undefined removes the key.
return null
}
The useMMKVObject hook handles JSON serialization for you, so you read and write plain objects without touching JSON.parse. If you only want to react to changes without binding a value, useMMKVListener gives you a listener-style subscription instead.
Locking It Down With Encryption
Sensitive values, auth tokens, personal data, license keys, deserve more than plaintext on disk. MMKV supports AES encryption either at instance creation or toggled at runtime.
import { createMMKV } from 'react-native-mmkv'
export const secureStore = createMMKV({
id: 'secure-storage',
encryptionKey: 'hunter2',
encryptionType: 'AES-256',
})
// Or change encryption later on an existing instance
secureStore.encrypt('rotated-key', 'AES-256')
secureStore.decrypt() // remove encryption entirely
One honest caveat: the encryption key still has to live somewhere on the device. For genuine hardware-backed secret storage, pair MMKV with the platform Keychain or Keystore, or use expo-secure-store to hold the MMKV key itself, then keep everything else in your encrypted MMKV instance. That combination, a small hardware-protected secret guarding a fast encrypted general store, is a common and pragmatic pattern.
Many Stores, One Logout, and Migration
A single configuration call unlocks the powerful side of MMKV: multiple isolated instances. Each id is a fully separate store with its own path, encryption, and process mode.
import { createMMKV, deleteMMKV } from 'react-native-mmkv'
export const settings = createMMKV({ id: 'app-settings' })
export function userStore(userId: string) {
return createMMKV({
id: `user-${userId}-storage`,
encryptionKey: deriveKey(userId),
encryptionType: 'AES-256',
})
}
// On logout, wipe just that user's data, untouched global settings remain
function logout(userId: string) {
deleteMMKV(`user-${userId}-storage`)
}
This per-user scoping makes account switching and logout clean: blow away one instance and your global preferences survive. The configuration object also accepts a custom path, a 'multi-process' mode for sharing data with app extensions and iOS App Groups, a readOnly flag, and compareBeforeSet. That last option is a nice IO optimization, when enabled, MMKV reads the existing value first and only writes if it actually changed, sparing redundant disk writes for unchanged values.
If you are coming from AsyncStorage, the official migration guide recommends a one-time copy guarded by a flag stored in MMKV itself.
import AsyncStorage from '@react-native-async-storage/async-storage'
import { storage } from './storage'
export async function migrateFromAsyncStorage() {
if (storage.getBoolean('hasMigratedFromAsyncStorage')) 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.removeItem(key)
}
}
storage.set('hasMigratedFromAsyncStorage', true)
}
Run that inside a useEffect in your root component, ideally wrapped in InteractionManager.runAfterInteractions() so it does not block the first frame, and show a brief loading indicator while it works. Because MMKV exposes a setItem / getItem / removeItem-shaped surface with a thin wrapper, writing adapters for Redux Persist or Zustand's persist middleware is almost trivial, which is why so many state libraries quietly run on MMKV underneath.
When to Reach for It, and When Not To
MMKV is the modern, synchronous, far faster replacement for AsyncStorage, and for the common job of storing settings, flags, tokens, and cached data, it is hard to beat. AsyncStorage's remaining advantages are mostly zero native configuration and Expo Go support, both of which matter for quick prototypes but fade once you ship a real build.
Just remember what it is not. MMKV is a key-value store, not a queryable database, so for relational data, complex queries, or large structured datasets you still want SQLite, op-sqlite, WatermelonDB, or Realm. And for hardware-backed secrets specifically, lean on the Keychain or expo-secure-store rather than MMKV's at-rest encryption alone. Used for the job it was built for, though, MMKV makes persistence feel effortless, fast, synchronous, typed, and quietly out of your way. Once you have read a value without awaiting it, going back is genuinely hard.