What is react-native-mmkv, and why can a React Native app read from it without await, unlike Async Storage?
answer
- C++ MMKV core, JS bindings
- JSI call, no Promise
- memory-mapped file, typed values
- missing key returns undefined
- sync means small values only
basics
~20 sreact-native-mmkv calls the native C++ MMKV library directly through JSI, so getString or set run synchronously on the JS thread and return a value, not a Promise. Async Storage's native calls are asynchronous and must be awaited.
solid answer
~40 sreact-native-mmkv wraps MMKV, a small C++ key-value library that keeps its data in a memory-mapped file. In v4 the JavaScript object you get from `createMMKV()` is a Nitro Module hybrid object, reached through JSI, so `storage.getString('todos')` is an ordinary synchronous function call that returns the value (or `undefined`) immediately. There is no Promise and no await, which means a screen can read its saved state during the very first render. Values are typed: `set` accepts strings, numbers, booleans and `ArrayBuffer`s, read back with `getString`, `getNumber`, `getBoolean` and `getBuffer`. Async Storage, by contrast, returns Promises for every call. The cost of synchronous access is that it runs on the JS thread, so it suits small values, not megabytes.
code
tsx · 28 linesimport { useState } from "react";
import { FlatList, Text } from "react-native";
import { createMMKV } from "react-native-mmkv";
type Todo = { id: string; title: string; done: boolean };
export const storage = createMMKV();
function loadTodos(): Todo[] {
const raw = storage.getString("todos");
if (raw === undefined) return [];
try {
return JSON.parse(raw) as Todo[];
} catch {
return [];
}
}
export function TodoList() {
const [todos] = useState<Todo[]>(loadTodos);
return (
<FlatList
data={todos}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <Text>{item.title}</Text>}
/>
);
}go deeper
Recall that react-native-mmkv calls are synchronous, that set takes strings, numbers, booleans or buffers, and that a missing key reads as undefined.
Explain the JSI call path and the memory-mapped file behind synchronous reads, and contrast them with Async Storage's Promise-based native calls.
Show how synchronous reads remove loading states on the startup path and where they turn into JS-thread cost, and name the native rebuild the library requires.
Judge where synchronous storage belongs in the app: startup-critical settings yes, large documents no, and how that split shapes the persistence layer.
## What the library is **MMKV** is a compact key-value storage library written in C++ for mobile apps. **react-native-mmkv** exposes it to React Native. The pinned version, **4.3.2**, is a **Nitro Module**: the storage object is a native "hybrid object" that JavaScript talks to through **JSI** (the JavaScript Interface), the C++ layer that lets JavaScript call native functions directly instead of posting asynchronous messages. ```typescript import { createMMKV } from "react-native-mmkv"; export const storage = createMMKV(); // default id: "mmkv.default" storage.set("showCompleted", true); const showCompleted = storage.getBoolean("showCompleted"); // true, no await ``` The README recommends creating an instance once and exporting it, rather than creating a new one on every call. ## Why no await is needed Two properties combine: 1. **A direct call path.** A JSI call is a plain C++ function invocation from the JavaScript engine. The function runs to completion on the JS thread and returns its result, exactly like calling a JavaScript function. 2. **Data already in memory.** MMKV maps its file into memory, so a read is a lookup in memory, not a disk round trip that would need to be awaited. The result is that `getString` returns `string | undefined` directly. Async Storage's `getItem` returns `Promise<string | null>` because its native side does its work off the JS thread and resolves later. ## The API in one table | Task | react-native-mmkv v4 | Async Storage v3 | |---|---|---| | write | `storage.set(key, value)` | `await storage.setItem(key, string)` | | read a string | `storage.getString(key)` | `await storage.getItem(key)` | | read other types | `getNumber`, `getBoolean`, `getBuffer` | parse from a string | | missing key | `undefined` | `null` | | key exists? | `storage.contains(key)` | compare the read with `null` | | delete | `storage.remove(key)` returns a boolean | `await storage.removeItem(key)` | | list / wipe | `getAllKeys()`, `clearAll()` | `await getAllKeys()`, `await clear()` | Objects still go through JSON: `storage.set("todos", JSON.stringify(todos))` and `JSON.parse(storage.getString("todos") ?? "[]")`. ## Reading typed values correctly MMKV getters are **typed**, and the getter must match how the value was written: - A value written with `set("count", 3)` is read with `getNumber("count")`. - A value written with `set("showCompleted", true)` is read with `getBoolean`. - `contains(key)` answers "is anything stored under this key?" without reading it. - `remove(key)` returns `true` only when a key was actually removed. Because every getter returns `undefined` for a missing key, a default is usually supplied at the call site: `storage.getBoolean("showCompleted") ?? false`. Using `||` instead would also replace a stored `false`, which is a classic bug with boolean settings. ## What synchronous access buys on the startup path In a to-do app that loads its list from Async Storage, the first render cannot have the data; the screen shows a spinner or an empty list, then fills in. With MMKV the state can be initialised synchronously: ```tsx const [todos, setTodos] = useState<Todo[]>(() => JSON.parse(storage.getString("todos") ?? "[]") ); ``` The first frame already shows the saved list: no loading state, no flash, no extra render. ## The trade-offs - **Work on the JS thread.** Every call blocks JavaScript until it returns. For flags, settings and small JSON documents that is negligible; for very large values, or thousands of writes in a loop, it competes with rendering and gestures. - **Native code required.** The package and its peer `react-native-nitro-modules` contain native code, so installing them means a native rebuild (`pod install`, or `npx expo prebuild` / a new development build in Expo projects). - **Plaintext by default.** Without an `encryptionKey`, values sit in the file as plain text and rely on the operating system's sandbox. - **Web is different.** On React Native Web the library stores values in `localStorage` under an id prefix, and falls back to a non-persistent in-memory map if `localStorage` is unavailable. ## A note on debugging Because calls are synchronous JSI calls, they only work when JavaScript runs inside the app's own engine. That was a problem for the old remote JS debugging, which executed JavaScript elsewhere; that mode is gone in current React Native, where React Native DevTools debugs the on-device Hermes engine, so it is no longer a practical concern.
- If MMKV reads are synchronous, can it still cause jank?Yes, when misused. Each call blocks the JS thread until it returns, so writing a large JSON document on every keystroke, or looping over thousands of keys during a gesture, competes with rendering. Keep values small, batch work outside interactions, and move large or query-shaped data to a database.
- Why does installing react-native-mmkv require rebuilding the app?The library and its `react-native-nitro-modules` peer ship native C++, iOS and Android code that must be compiled into the binary. A JavaScript-only reload cannot add it, so you run `pod install` and rebuild, or run `npx expo prebuild` or create a new development build in an Expo project.
Async Storage is like asking a stock clerk to fetch an item from the back room: you hand over a note and wait for them to come back. MMKV keeps the shelf behind the counter within arm's reach, so you just take the item, but only small items fit on that shelf.
saying these in an interview costs you the question
- Awaiting storage.getString as if it returned a Promise
- Testing a missing MMKV key with === null instead of undefined
- Saying MMKV is fast because it caches values in a JavaScript object
- Believing synchronous calls are free, whatever the value size
- Assuming MMKV encrypts data without an encryptionKey