MMKV în React Native: Ghid Complet pentru Storage Local Rapid și Migrarea din AsyncStorage în 2026
Ghid complet pentru react-native-mmkv în 2026: API sincron, migrare din AsyncStorage, criptare AES, integrare Zustand și TanStack Query, cu exemple de cod production-ready dintr-o aplicație fintech.
react-native-mmkv este cea mai rapidă bibliotecă de tip cheie-valoare pentru React Native în 2026: oferă un API sincron construit peste JSI, este de aproximativ 30 de ori mai rapid decât AsyncStorage și include criptare AES nativă. Dacă rulezi un fintech, un e-commerce sau orice aplicație unde starea persistată blochează primul render, MMKV e alegerea implicită. În continuare îți arăt cum îl instalez în producție, cum migrez datele din AsyncStorage fără să pierd nimic și cum îl integrez type-safe cu Zustand și TanStack Query.
MMKV v3 rulează peste JSI ca TurboModule / Nitro Module și necesită Noua Arhitectură React Native activă.
API-ul este sincron: storage.set('key', value) nu returnează Promise, deci elimini await din codul de hidratare.
Benchmark oficial: ~30x mai rapid decât @react-native-async-storage/async-storage pentru scrieri de valori mici.
Criptarea AES-128 se activează cu o singură opțiune encryptionKey, dar cheia trebuie stocată în Keychain/Keystore via expo-secure-store.
MMKV nu funcționează în Expo Go: necesită prebuild și un dev client custom, ceea ce e acceptabil pentru majoritatea proiectelor de producție.
Se integrează cu Zustand prin adaptorul StateStorage și cu TanStack Query prin persistQueryClient pentru cache offline instant.
Ce este react-native-mmkv și de ce contează în 2026
MMKV este o bibliotecă de storage cheie-valoare dezvoltată inițial de echipa WeChat și portată la React Native de Marc Rousavy prin pachetul react-native-mmkv. Sub capotă folosește mmap (memory-mapped I/O): sistemul de operare mapează fișierul de storage direct în spațiul de adrese al procesului, iar scrierile devin operațiuni de memorie, nu apeluri de sistem sincronizate. În combinație cu bindings JSI compilate C++, rezultatul e că storage.getString('user') execută în ordinul microsecundelor, sincron, fără serializare JSON și fără bridge asincron.
Am migrat trei aplicații fintech de la AsyncStorage la MMKV în ultimul an, și diferența practică nu se vede doar în benchmark. Când starea persistată alimentează primul render (temă, locale, flag de onboarding, ultima rută vizitată), lipsa lui await înseamnă că poți hidrata store-ul înainte să monezi React tree-ul. Splash screen-ul dispare cu 200-400 ms mai devreme pe device-uri mid-range și nu mai vezi flash-ul temei light peste tema dark. Pentru aplicații fintech unde primul screen e login-ul plus balanța locală cache-uită, câștigul e vizibil imediat.
Versiunea 3.x rulează exclusiv pe Noua Arhitectură React Native și oferă bindings prin Nitro Modules. Dacă proiectul tău încă rulează pe bridge-ul vechi, poți rămâne pe v2.x, dar recomand cu tărie să faci upgrade: beneficiul de performanță e marginal pe v2 și pierzi optimizările Nitro. Pentru context complet despre Noua Arhitectură, vezi ghidul complet despre JSI, Fabric și TurboModules.
MMKV vs AsyncStorage vs expo-secure-store: matricea de decizie
Cea mai frecventă întrebare pe care o primesc de la echipe care fac audit înainte de migrare este „care e diferența practică între cele trei?". Răspunsul scurt: nu sunt înlocuitori unul pentru altul, ci componente complementare. Regula pe care o aplic în platforma noastră este MMKV pentru viteză, SecureStore pentru secrete, AsyncStorage doar pentru compatibilitate cu Expo Go.
Caracteristică
react-native-mmkv
AsyncStorage
expo-secure-store
Viteză scriere (100 iterații)
~2 ms
~60 ms
~250 ms
Tip API
Sincron
Asincron (Promise)
Asincron (Promise)
Criptare
Opțională (AES-128/256)
Nu
Obligatorie (Keychain/Keystore)
Tipuri suportate nativ
string, number, boolean, ArrayBuffer
string
string
Compatibilitate Expo Go
Nu (necesită prebuild)
Da
Da
Suport Noua Arhitectură
Obligatoriu în v3.x
Da
Da
Caz de utilizare recomandat
State app, cache, preferințe
Legacy sau prototipare rapidă
Token-uri, chei API, secrete
În arhitectura pe care o rulez la fintech, distribuția e clară. Token-ul de acces și refresh-ul JWT merg în expo-secure-store (protejate de Secure Enclave pe iOS și Keystore hardware-backed pe Android), iar cheia de criptare AES pentru MMKV e de asemenea stocată acolo. MMKV primește restul: starea Zustand persistată, cache-ul TanStack Query, feature flags cache-uite de la Firebase Remote Config, ultima rută vizitată, tema aleasă și lista de tranzacții recente pentru instant-render. AsyncStorage nu mai există în codebase.
Instalare și configurare cu Expo prebuild
MMKV necesită cod nativ (C++/Objective-C++/Kotlin), deci nu rulează în Expo Go. Fluxul standard în 2026 e să folosești Expo Prebuild plus un dev client custom, ceea ce îți păstrează beneficiile Expo (EAS Build, EAS Update, expo-router) fără restricțiile Expo Go.
Diferența filozofică față de AsyncStorage e că MMKV nu forțează serializare. Ai patru getter/setter tipați: String, Number, Boolean și Buffer (pentru ArrayBuffer). Nu există getObject, deci pentru obiecte trebuie să faci JSON.stringify/parse manual, ceea ce în practică e un plus: te forțează să declari o schemă și un validator (Zod, valibot) în loc să pasezi any.
Un wrapper type-safe pe care îl folosesc în fiecare proiect arată așa:
// storage/mmkv.ts
import { MMKV } from 'react-native-mmkv';
import { z } from 'zod';
export const storage = new MMKV({
id: 'app-storage',
});
// Definește schema pentru fiecare cheie
const UserPreferencesSchema = z.object({
theme: z.enum(['light', 'dark', 'system']),
locale: z.string(),
onboardingCompleted: z.boolean(),
lastRoute: z.string().optional(),
});
type UserPreferences = z.infer<typeof UserPreferencesSchema>;
const PREFS_KEY = 'user.preferences.v1';
export function getUserPreferences(): UserPreferences | null {
const raw = storage.getString(PREFS_KEY);
if (!raw) return null;
const parsed = UserPreferencesSchema.safeParse(JSON.parse(raw));
return parsed.success ? parsed.data : null;
}
export function setUserPreferences(prefs: UserPreferences): void {
storage.set(PREFS_KEY, JSON.stringify(prefs));
}
export function clearUserPreferences(): void {
storage.delete(PREFS_KEY);
}
Sufixul .v1 din numele cheii e o convenție pe care o aplic religios: când schema se schimbă (adaugi un câmp obligatoriu, redenumești unul), incrementezi la .v2 și scrii un migrator. Fără versiune, primul deploy care schimbă schema va crash-ui aplicația pentru toți utilizatorii cu preferințe vechi. Zod cu safeParse este plasa de siguranță: dacă vine ceva neașteptat, returnezi null și lași aplicația să folosească default-urile.
Cum migrez complet din AsyncStorage la MMKV
Migrarea este cea mai delicată parte a proiectului pentru că trebuie să se întâmple o singură dată, la prima pornire după deploy, și trebuie să fie idempotentă. Documentația oficială din MIGRATE_FROM_ASYNC_STORAGE.md propune un pattern pe care l-am rafinat pentru producție:
// storage/migrateAsyncStorage.ts
import AsyncStorage from '@react-native-async-storage/async-storage';
import { storage } from './mmkv';
const MIGRATION_FLAG = '__migrated_from_asyncstorage_v1';
export async function migrateFromAsyncStorage(): Promise<void> {
if (storage.getBoolean(MIGRATION_FLAG)) return;
const keys = await AsyncStorage.getAllKeys();
const startedAt = performance.now();
for (const key of keys) {
try {
const value = await AsyncStorage.getItem(key);
if (value == null) continue;
// Detectează tipuri primitive comune
if (value === 'true' || value === 'false') {
storage.set(key, value === 'true');
} else if (/^-?\d+(\.\d+)?$/.test(value)) {
storage.set(key, Number(value));
} else {
storage.set(key, value);
}
} catch (err) {
console.warn(`[MMKV migration] key "${key}" failed`, err);
// Nu re-throw. O singură cheie coruptă nu trebuie să blocheze restul
}
}
storage.set(MIGRATION_FLAG, true);
// Șterge AsyncStorage doar după ce am marcat migrarea ca fiind completă
await AsyncStorage.clear();
const durationMs = performance.now() - startedAt;
console.log(`[MMKV] migrated ${keys.length} keys in ${durationMs.toFixed(0)}ms`);
}
Apoi apelezi funcția înainte de a monta React root-ul, pentru ca hidratarea Zustand să vadă deja datele în MMKV:
Ordinea contează: flag-ul de migrare se scrie în MMKV înainte să faci AsyncStorage.clear(). Dacă app-ul crash-uiește între cele două operațiuni, la următoarea pornire flag-ul e deja setat, migrarea nu se repetă, dar și AsyncStorage e curat, așa că nu pierzi date pentru că le-ai copiat deja. Pentru datele critice (starea coșului, tranzacții unsynced), rulez migrarea o singură dată pe utilizator și păstrez AsyncStorage necurățat pentru două release-uri, ca fallback dacă descopăr un bug.
Instanțe multiple: separarea per-user vs global
Un anti-pattern pe care îl văd des în code-review e să pui totul într-o singură instanță MMKV globală. În aplicațiile multi-tenant (fintech cu profiluri multiple, apps cu switch de cont) trebuie să separi datele per utilizator, altfel logout-ul devine un coșmar. Trebuie să știi exact ce chei sunt globale (temă, locale) și ce chei sunt per-user (cache tranzacții, preferințe notificări). Pentru un context mai larg despre autentificare și profil switching, vezi ghidul de autentificare cu Expo Router și rute protejate.
// storage/instances.ts
import { MMKV } from 'react-native-mmkv';
import * as SecureStore from 'expo-secure-store';
export const globalStorage = new MMKV({ id: 'global' });
// Cache-uim instanțele per-user ca să nu creăm handle nou la fiecare acces
const userStorages = new Map<string, MMKV>();
export function getUserStorage(userId: string): MMKV {
let instance = userStorages.get(userId);
if (!instance) {
// Fiecare user primește propria cheie de criptare
const encryptionKey = getOrCreateUserKey(userId);
instance = new MMKV({
id: `user-${userId}`,
encryptionKey,
});
userStorages.set(userId, instance);
}
return instance;
}
function getOrCreateUserKey(userId: string): string {
const keyName = `mmkv_key_${userId}`;
let key = SecureStore.getItem(keyName);
if (!key) {
key = generateRandomKey(32);
SecureStore.setItem(keyName, key);
}
return key;
}
export function wipeUser(userId: string): void {
const instance = getUserStorage(userId);
instance.clearAll();
userStorages.delete(userId);
SecureStore.deleteItemAsync(`mmkv_key_${userId}`);
}
La logout apelezi wipeUser și toate datele acelui cont dispar. Fișierul MMKV este șters și cheia din Secure Store la fel. Datele globale (temă, locale) rămân neatinse, ceea ce e comportamentul așteptat.
Criptare AES și gestionarea sigură a cheilor
Criptarea MMKV este opt-in. Dacă nu treci encryptionKey, datele sunt scrise în plain-text (rapid, dar accesibile pentru oricine cu acces la sandbox-ul aplicației, pe device-uri jailbroken sau rooted, sau prin backup-uri iCloud/ADB). Pentru orice date sensibile chiar și non-secrete (istoric căutări, cache profil, preferințe cu implicații privacy), activez criptarea implicit.
// storage/encrypted.ts
import { MMKV } from 'react-native-mmkv';
import * as SecureStore from 'expo-secure-store';
import * as Crypto from 'expo-crypto';
const MMKV_KEY_NAME = 'mmkv-master-key';
function getOrCreateMasterKey(): string {
let key = SecureStore.getItem(MMKV_KEY_NAME);
if (!key) {
// 256 biți entropie
const bytes = Crypto.getRandomBytes(32);
key = Buffer.from(bytes).toString('base64');
SecureStore.setItem(MMKV_KEY_NAME, key, {
keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
});
}
return key;
}
export const secureStorage = new MMKV({
id: 'secure-app-storage',
encryptionKey: getOrCreateMasterKey(),
});
Câteva detalii pe care le-am învățat pe pielea mea:
Nu schimba encryptionKey între release-uri. MMKV nu poate decripta datele vechi cu o cheie nouă. Dacă chiar trebuie să rotești cheia, folosește storage.recrypt(newKey) care re-scrie tot fișierul.
Setează keychainAccessible la WHEN_UNLOCKED_THIS_DEVICE_ONLY pentru chei. Asta previne restaurarea pe un device nou din backup, ceea ce ar face aplicația să nu mai poată decripta storage-ul migrat.
Nu apela SecureStore.getItem la fiecare acces MMKV. Cache-uiește cheia în memorie la bootstrap. SecureStore este de ~250x mai lent decât MMKV; dacă o citești la fiecare read, pierzi tot avantajul de performanță.
Integrare cu Zustand persist middleware
Combinația MMKV + Zustand este stack-ul meu default pentru state management în orice React Native în 2026 (vezi ghidul complet Zustand pentru React Native pentru contextul mai larg). Persist middleware-ul Zustand cere un StateStorage adapter cu semnătură asincronă, dar MMKV este sincron, ceea ce e perfect: returnezi valorile direct, wrappate în Promise.resolve.
// stores/createMMKVStorage.ts
import type { StateStorage } from 'zustand/middleware';
import { storage } from '@/storage/mmkv';
export const mmkvStorage: StateStorage = {
setItem: (name, value) => {
storage.set(name, value);
},
getItem: (name) => {
const value = storage.getString(name);
return value ?? null;
},
removeItem: (name) => {
storage.delete(name);
},
};
Pentru echipele care nu vor să scrie manual adaptorul, pachetul zustand-mmkv-storage îl oferă gata făcut, cu suport pentru instance caching și hydration detection. În proiectele mele prefer versiunea scrisă manual pentru control complet asupra logicii de migrare.
Persistarea cache-ului TanStack Query cu MMKV
Cache-ul TanStack Query este un candidat perfect pentru MMKV: e citit o singură dată la bootstrap, apoi scris frecvent în background când sosesc răspunsuri fresh. Combinația dă app-uri offline-first cu experiență instant. Utilizatorul deschide aplicația, vede imediat datele din ultima sesiune, iar UI-ul se rehidratează cu date fresh pe măsură ce request-urile de rețea completează. Vezi ghidul complet TanStack Query pentru React Native pentru contextul de data fetching.
Setarea critică este buster. Acționează ca un cache-key global. Când schimbi schema unui query (adaugi un câmp în răspunsul serializat, redenumești o cheie), incrementezi buster la v2 și tot cache-ul persistat este invalidat automat la următorul boot. Utilizatorii nu primesc un obiect vechi cu shape incompatibil care crash-uiește selector-ul.
Debugging și probleme frecvente în producție
Șase probleme pe care le văd repetitiv la echipele care adoptă MMKV:
1. „Cannot find symbol MMKV" la build-ul iOS
Aproape întotdeauna înseamnă că pod install nu a rulat după instalarea pachetului sau că cache-ul CocoaPods e stale. Rulează cd ios && pod deintegrate && pod install && cd .. și apoi rebuild.
2. „TurboModuleRegistry.getEnforcing MMKV could not be found"
Noua Arhitectură nu este activată. Verifică newArchEnabled: true în app.json și în ios/Podfile.properties.json setează "newArchEnabled": "true". Pe Android, în gradle.properties pune newArchEnabled=true.
3. Datele dispar între build-uri de dev
În dev, dacă schimbi id-ul instanței MMKV sau encryptionKey, se creează un fișier nou și cel vechi devine inaccesibil. Nu e un bug, e cum funcționează izolarea instanțelor. În producție nu se întâmplă pentru că nu schimbi acele valori.
4. Migrarea din AsyncStorage rulează de mai multe ori
De obicei înseamnă că apelezi migrateFromAsyncStorage() dintr-un component effect (useEffect) în loc de bootstrap. React Native poate reinstantia component tree-ul la Fast Refresh, iar effect-ul rulează din nou. Apelează migrarea în index.js, înainte de AppRegistry.registerComponent. Am pierdut o jumătate de zi la primul proiect exact pe bug-ul ăsta.
5. Performanța nu se îmbunătățește după migrare
Probabil folosești în continuare createJSONStorage peste MMKV cu Zustand și storezi obiecte imense. Sincronizarea nu ajută dacă serializarea JSON durează 50ms. Splitează store-ul în bucăți mai mici sau folosește partialize pentru a persista doar câmpurile care contează.
6. Testare cu Jest
MMKV nu are backend Node, deci în Jest trebuie mock. Cel mai simplu: creează un mock in-memory în __mocks__/react-native-mmkv.ts care implementează interfața cu un Map. Există și pachetul react-native-mmkv-mock menținut de comunitate, dar prefer un mock local pentru control complet asupra semanticii.
// __mocks__/react-native-mmkv.ts
class MockMMKV {
private store = new Map<string, string | number | boolean>();
set(key: string, value: string | number | boolean) { this.store.set(key, value); }
getString(key: string) { const v = this.store.get(key); return typeof v === 'string' ? v : undefined; }
getNumber(key: string) { const v = this.store.get(key); return typeof v === 'number' ? v : undefined; }
getBoolean(key: string) { const v = this.store.get(key); return typeof v === 'boolean' ? v : undefined; }
delete(key: string) { this.store.delete(key); }
clearAll() { this.store.clear(); }
getAllKeys() { return Array.from(this.store.keys()); }
}
export const MMKV = MockMMKV;
Întrebări frecvente
Este MMKV într-adevăr de 30 de ori mai rapid decât AsyncStorage?
Da, pentru scrieri de valori mici (sub 1KB) benchmark-ul oficial arată aproximativ 30x. Diferența vine din faptul că MMKV folosește mmap și JSI (sincron, în-proces), pe când AsyncStorage face round-trip prin bridge și scrie în SQLite pe iOS sau într-o bază NitroSQLite pe Android. Pentru scrieri mari (peste 100KB), diferența se reduce către 5-10x pentru că serializarea JSON și copierea de buffer devin dominante.
Pot folosi MMKV în Expo Go?
Nu. MMKV conține cod nativ care nu este inclus în Expo Go. Trebuie să rulezi npx expo prebuild și să folosești un dev client custom (npx expo run:ios sau run:android). Este alegerea corectă pentru producție oricum, pentru că Expo Go nu suportă majoritatea bibliotecilor native pe care le vei folosi într-o aplicație reală.
Cum stochez token-uri de autentificare în MMKV în siguranță?
Nu o faci. Token-urile de acces și refresh trebuie să meargă în expo-secure-store, care le protejează cu Secure Enclave pe iOS și Keystore hardware-backed pe Android. MMKV este pentru state al aplicației, cache și preferințe. Chiar și cu encryptionKey, cheia însăși trebuie stocată undeva sigur, iar acel „undeva" este exact SecureStore.
Ce se întâmplă cu datele MMKV la dezinstalarea aplicației?
Datele sunt șterse cu aplicația pe ambele platforme. MMKV scrie în sandbox-ul app-ului, care este eliminat la dezinstalare. Pe Android, dacă utilizatorul face „Clear Data" din setări, datele MMKV dispar, dar aplicația rămâne instalată. Testează explicit acest scenariu: flag-ul de migrare din AsyncStorage va fi resetat și migrarea va încerca să ruleze din nou (care va fi no-op, pentru că și AsyncStorage este gol).
MMKV suportă valori observabile sau listeners de schimbare?
Da. Începând cu v2.5+, storage.addOnValueChangedListener(callback) declanșează callback-ul când orice cheie este scrisă. Pentru un singur key, filtrezi în callback. Există și hook-urile useMMKVString, useMMKVNumber, useMMKVBoolean, useMMKVObject care se sincronizează automat cu React state. Pentru integrare cu Zustand, hook-urile Zustand sunt suficiente și nu ai nevoie de listener-ii MMKV direct.
Nitro Modules aduc module native ultra-rapide în React Native prin binding JSI compilat static: apeluri de până la 15x mai rapide decât TurboModules, cu type-safety la build și cod idiomatic în Swift și Kotlin.
Ghid practic pentru expo-image în React Native: benchmark-uri vs FastImage, cache memory/disk, BlurHash și ThumbHash, WebP/AVIF, prefetch cu prioritate și profilare în React Native DevTools.
Cum folosești TanStack Query v5 în React Native și Expo pentru data fetching, caching automat, mutații cu optimistic updates și persistență offline cu MMKV.