mobile-storage-mmkv
MMKV high-performance key-value storage for React Native - synchronous JSI-based reads/writes, encryption, typed hooks, multiple instances, listeners, persistence middleware adapters
Install
npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/mobile-storage-mmkv/skills/mobile-storage-mmkv
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
git clone https://github.com/agents-inc/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
MMKV Storage Patterns
Quick Guide: Use
createMMKV()for synchronous key-value storage (~30x faster than AsyncStorage). One singleton instance per concern (global app, per-user). Use typed hooks (useMMKVString,useMMKVObject) for reactive components. Enable encryption withencryptionKeyfor sensitive data. V4 is a Nitro Module requiringreact-native-nitro-modulesand React Native 0.75+.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST reuse a single MMKV instance per concern -- NEVER call createMMKV() on every render or in component bodies)
(You MUST use typed getters (getString, getNumber, getBoolean) -- NEVER parse the return value of the wrong getter)
(You MUST use remove() to delete keys -- delete() was renamed in v4 due to C++ keyword conflict)
(You MUST install react-native-nitro-modules alongside react-native-mmkv -- v4 is a Nitro Module)
</critical_requirements>
Auto-detection: MMKV, react-native-mmkv, createMMKV, useMMKVString, useMMKVNumber, useMMKVBoolean, useMMKVObject, useMMKVBuffer, useMMKVListener, useMMKVKeys, addOnValueChangedListener, encryptionKey, mmkv storage, key-value storage React Native
When to use:
- Persisting user preferences, auth tokens, or cached data synchronously
- Replacing AsyncStorage for faster reads/writes (~30x improvement)
- Encrypting sensitive data at rest with AES-128 or AES-256
- Sharing storage between iOS app and extensions via App Groups
- Building reactive UIs that re-render on storage changes (hooks)
- Isolating data per user or feature with multiple named instances
Key patterns covered:
- Instance creation with
createMMKV()and configuration options - Typed getters/setters and object serialization
- React hooks for reactive storage (
useMMKVString,useMMKVObject, etc.) - Value change listeners (
addOnValueChangedListener,useMMKVListener) - Multiple instances for data isolation (global vs per-user)
- Encryption at rest (AES-128/AES-256)
- Persistence middleware adapter (generic
StateStorageinterface) - Migration from AsyncStorage
When NOT to use:
- Large binary files or media (use the filesystem)
- Relational or queryable data (use a local database)
- Data that must sync across devices (use a cloud-synced solution)
- Server state caching with invalidation (use your data fetching layer)
Detailed Resources:
- examples/core.md - Instance setup, typed access, hooks, listeners
- examples/advanced.md - Encryption, multiple instances, App Groups, multi-process, migration
- examples/persistence.md - State management persistence adapter, hydration handling
- reference.md - API reference, V3-to-V4 migration table, migration checklist
<decision_framework>
Decision Framework
What kind of data are you storing?
|
+-> Key-value pairs (strings, numbers, booleans, small objects)?
| +-> Sensitive data (tokens, keys, PII)?
| | +-> YES -> MMKV with encryptionKey
| | +-> NO -> MMKV without encryption
| +-> Need reactive UI updates?
| | +-> YES -> Use MMKV hooks (useMMKVString, etc.)
| | +-> NO -> Use direct get/set API
| +-> Multiple users or data domains?
| +-> YES -> Multiple named instances
| +-> NO -> Single default instance
|
+-> Large files or binary media?
| +-> Use the filesystem (not MMKV)
|
+-> Relational data with queries?
| +-> Use a local database (not MMKV)
|
+-> Server-cached data with invalidation?
+-> Use your data fetching layer (not MMKV)
When to Use Each API Style
| Scenario | API |
|---|---|
| Read/write in services or utils | Direct: storage.getString() |
| Reactive component state | Hook: useMMKVString() |
| Cross-component sync | Hook or addOnValueChangedListener |
| Background task or service | Direct + listener |
| State management persistence | StateStorage adapter |
</decision_framework>
<red_flags>
RED FLAGS
High Priority Issues:
- Calling
createMMKV()inside a component body -- creates new native instance every render, use module-scope singleton - Using
storage.delete()-- renamed tostorage.remove()in v4,deleteis a C++ reserved keyword - Mixing typed getters --
getStringon a number key returnsundefined, not a string. Use the matching getter. - Missing
react-native-nitro-modulespeer dependency -- v4 crashes at runtime without it - Using v4 on React Native < 0.75 -- Nitro Modules require RN 0.75+
Medium Priority Issues:
- Storing large objects (>1MB) in MMKV -- designed for small key-value pairs, not large blobs
- Not calling
listener.remove()-- native listeners leak if not cleaned up - Using default instance for sensitive data without encryption -- device compromise exposes data
- Forgetting
encryptionType: "AES-256"when AES-256 is needed -- default is AES-128
Gotchas & Edge Cases:
- MMKV encryption applies to the entire instance, not individual keys -- use a separate encrypted instance for sensitive data
useMMKVObject<T>usesJSON.stringify/JSON.parseinternally -- objects withDate,Map,Setlose their types- Setting a hook value to
undefineddeletes the key from storage -- intentional API, not a bug - Remote JS debugging (Chrome DevTools) does not work with MMKV -- JSI requires on-device execution. Use Flipper or React DevTools
compareBeforeSetoption prevents writing if value is unchanged -- useful for reducing disk I/O in high-frequency updates- iOS App Groups require
AppGroupIdentifierin Info.plist (wasAppGroupin v3) andmode: "multi-process" - MMKV provides automatic test mocks --
createMMKV()works in test runners without native compilation getAllKeys()returns all keys as an array -- there is no prefix filtering, implement it yourself if neededstorage.sizereturns bytes used -- callstorage.trim()to reclaim space from deleted keys
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md
(You MUST reuse a single MMKV instance per concern -- NEVER call createMMKV() on every render or in component bodies)
(You MUST use typed getters (getString, getNumber, getBoolean) -- NEVER parse the return value of the wrong getter)
(You MUST use remove() to delete keys -- delete() was renamed in v4 due to C++ keyword conflict)
(You MUST install react-native-nitro-modules alongside react-native-mmkv -- v4 is a Nitro Module)
Failure to follow these rules will cause memory leaks, runtime crashes, or silent data loss.
</critical_reminders>
Files (skills)
-
examples
-
advanced.md 7.4 KB
# MMKV - Advanced Patterns > Encryption, multiple instances, App Groups, multi-process mode, and AsyncStorage migration. See [SKILL.md](../SKILL.md) for decision guidance. See [examples/core.md](core.md) for basic usage. --- ## Pattern 1: Encryption with AES-128 and AES-256 Encryption applies to the **entire instance** -- you cannot encrypt individual keys. Use a dedicated encrypted instance for sensitive data. ```typescript import { createMMKV } from "react-native-mmkv"; // AES-128 encryption (default when encryptionKey is provided) const secureStorage = createMMKV({ id: "secure", encryptionKey: "your-secret-key", }); // AES-256 encryption (stronger, slightly slower) const highSecurityStorage = createMMKV({ id: "high-security", encryptionKey: "your-secret-key", encryptionType: "AES-256", }); ``` ### Runtime Encryption Management ```typescript // Encrypt an existing unencrypted instance storage.encrypt("new-password"); // Upgrade encryption type storage.encrypt("new-password", "AES-256"); // Remove encryption (data becomes plaintext) storage.decrypt(); ``` ### Key Rotation Pattern ```typescript const rotateEncryptionKey = ( storage: ReturnType<typeof createMMKV>, newKey: string, ) => { // Re-encrypting with a new key decrypts with the old key // and re-encrypts with the new key in one operation storage.encrypt(newKey, "AES-256"); }; ``` **When to use AES-128 vs AES-256:** | Scenario | Recommendation | | ---------------------------------- | -------------- | | User preferences, non-sensitive | No encryption | | Auth tokens, session data | AES-128 | | PII, financial data, API secrets | AES-256 | | Regulatory compliance (HIPAA, etc) | AES-256 | --- ## Pattern 2: Multiple Instances for Data Isolation ```typescript import { createMMKV, existsMMKV, deleteMMKV } from "react-native-mmkv"; const APP_STORAGE_ID = "app-global"; // Global storage -- app-level settings, feature flags export const appStorage = createMMKV({ id: APP_STORAGE_ID }); // Per-user storage -- created on login, deleted on logout export const createUserStorage = (userId: string) => createMMKV({ id: `user-${userId}`, encryptionKey: `user-key-${userId}`, }); // Instance lifecycle management export const userStorageExists = (userId: string): boolean => existsMMKV(`user-${userId}`); export const deleteUserStorage = (userId: string): boolean => deleteMMKV(`user-${userId}`); ``` ### Usage in Auth Flow ```typescript import { createUserStorage, deleteUserStorage } from "./storage"; let userStorage: ReturnType<typeof createMMKV> | null = null; const handleLogin = (userId: string) => { userStorage = createUserStorage(userId); // User-specific data is now isolated userStorage.set("lastLoginAt", Date.now()); }; const handleLogout = (userId: string) => { // Delete entire user storage -- all keys removed from disk deleteUserStorage(userId); userStorage = null; }; ``` **Why good:** `deleteMMKV` removes the entire storage file from disk -- no leftover data after logout. `existsMMKV` checks without creating the instance. --- ## Pattern 3: iOS App Group Sharing Share MMKV data between your main app and extensions (widgets, watch, share extensions). ### Step 1: Configure App Group in Info.plist ```xml <key>AppGroupIdentifier</key> <string>group.com.yourcompany.yourapp</string> ``` > **V4 change:** The key was `AppGroup` in v3, renamed to `AppGroupIdentifier` in v4. ### Step 2: Create Shared Instance ```typescript import { createMMKV } from "react-native-mmkv"; const SHARED_STORAGE_ID = "shared-with-extensions"; // Both main app and extension use this same config export const sharedStorage = createMMKV({ id: SHARED_STORAGE_ID, mode: "multi-process", // Required for cross-process access }); ``` ### Step 3: Read/Write from Extension The extension uses the same `createMMKV` call with the same `id` and `mode: "multi-process"`. MMKV handles file locking and cross-process synchronization automatically. --- ## Pattern 4: Android Multi-Process Mode When your app uses multiple processes (services, content providers), enable multi-process mode to prevent data corruption. ```typescript const multiProcessStorage = createMMKV({ id: "multi-process-storage", mode: "multi-process", }); ``` **When to use:** App has background services in separate processes, or you are using Android App Widgets that run in a different process. **When NOT needed:** Single-process apps (most React Native apps are single-process). --- ## Pattern 5: Migration from AsyncStorage One-time migration script that copies all AsyncStorage data to MMKV, then cleans up. ```typescript import AsyncStorage from "@react-native-async-storage/async-storage"; import { createMMKV } from "react-native-mmkv"; const storage = createMMKV(); const MIGRATION_FLAG = "hasMigratedFromAsyncStorage"; export const hasMigrated = (): boolean => storage.getBoolean(MIGRATION_FLAG) === true; export const migrateFromAsyncStorage = async (): Promise<void> => { if (hasMigrated()) return; const start = performance.now(); const keys = await AsyncStorage.getAllKeys(); for (const key of keys) { try { const value = await AsyncStorage.getItem(key); if (value !== null) { // Detect booleans stored as strings if (value === "true" || value === "false") { storage.set(key, value === "true"); } else { storage.set(key, value); } await AsyncStorage.removeItem(key); } } catch (error) { console.error(`Failed to migrate key "${key}":`, error); // Continue with remaining keys -- don't abort entire migration } } storage.set(MIGRATION_FLAG, true); const elapsed = performance.now() - start; console.log(`MMKV migration completed in ${elapsed.toFixed(1)}ms`); }; ``` ### Integrate in App Entry Point ```typescript import { useState, useEffect } from "react"; import { InteractionManager, ActivityIndicator, View } from "react-native"; import { hasMigrated, migrateFromAsyncStorage } from "./migration"; function App() { const [ready, setReady] = useState(hasMigrated()); useEffect(() => { if (ready) return; // Run after animations complete to avoid janky transitions const task = InteractionManager.runAfterInteractions(async () => { await migrateFromAsyncStorage(); setReady(true); }); return () => task.cancel(); }, [ready]); if (!ready) { return ( <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}> <ActivityIndicator size="large" /> </View> ); } return <MainApp />; } export { App }; ``` **Why good:** Migration runs once (flag prevents re-runs), deferred to after interactions to avoid blocking UI, error handling per key prevents one bad key from aborting entire migration. --- ## Pattern 6: Storage Size and Maintenance ```typescript import { storage } from "./storage"; // Check storage size in bytes const sizeInBytes = storage.size; const TRIM_THRESHOLD = 4096; // Trim reclaims space from deleted keys if (sizeInBytes >= TRIM_THRESHOLD) { storage.trim(); } // Import data from another MMKV instance import { createMMKV } from "react-native-mmkv"; const legacyStorage = createMMKV({ id: "legacy" }); const importedCount = storage.importAllFrom(legacyStorage); ``` **Why good:** `trim()` is a lightweight operation that reclaims disk space. `importAllFrom` enables instance consolidation without manual key copying. -
core.md 7.3 KB
# MMKV - Core Patterns > Instance setup, typed access, React hooks, and listeners. See [SKILL.md](../SKILL.md) for decision guidance and red flags. **Prerequisites:** React Native 0.75+, `react-native-mmkv` v4+, `react-native-nitro-modules` installed. --- ## Pattern 1: Instance Creation with Full Configuration ```typescript import { createMMKV } from "react-native-mmkv"; // Default instance -- most apps only need this export const storage = createMMKV(); // Fully configured instance const SECURE_STORAGE_ID = "secure-storage"; export const secureStorage = createMMKV({ id: SECURE_STORAGE_ID, encryptionKey: "your-secret-key", encryptionType: "AES-256", // Default: "AES-128" // path: "/custom/path", // Custom file location (rarely needed) // mode: "multi-process", // For App Groups / extensions // readOnly: true, // Prevent writes // compareBeforeSet: true, // Skip write if value unchanged }); ``` **Why good:** Module-scope creation runs once. Named exports let consumers import the instance they need. Config options are documented inline. ```typescript // BAD: Instance in component body import { createMMKV } from "react-native-mmkv"; function SettingsScreen() { // New native allocation EVERY render const storage = createMMKV({ id: "settings" }); const theme = storage.getString("theme"); return <Text>{theme}</Text>; } ``` **Why bad:** `createMMKV()` allocates a native C++ object. Calling it in a component body creates a new instance on every render, leaking memory. --- ## Pattern 2: Typed Getters, Setters, and Key Management ```typescript import { storage } from "./storage"; // --- String --- storage.set("user.name", "Alice"); const name = storage.getString("user.name"); // "Alice" | undefined // --- Number --- const MAX_RETRIES = 5; storage.set("settings.maxRetries", MAX_RETRIES); const retries = storage.getNumber("settings.maxRetries"); // 5 | undefined // --- Boolean --- storage.set("onboarding.completed", true); const done = storage.getBoolean("onboarding.completed"); // true | undefined // --- ArrayBuffer (binary data) --- const encoder = new TextEncoder(); storage.set("cert", encoder.encode("binary-data").buffer); const cert = storage.getBuffer("cert"); // ArrayBuffer | undefined // --- Object (via JSON serialization) --- interface UserProfile { id: string; name: string; email: string; } const profile: UserProfile = { id: "1", name: "Alice", email: "alice@example.com", }; storage.set("user.profile", JSON.stringify(profile)); const stored = storage.getString("user.profile"); const parsed: UserProfile | undefined = stored ? JSON.parse(stored) : undefined; // --- Key management --- storage.contains("user.name"); // true storage.getAllKeys(); // ["user.name", "settings.maxRetries", ...] storage.remove("user.name"); // Delete single key (NOT .delete() -- renamed in v4) storage.clearAll(); // Delete all keys in this instance ``` **Why good:** Each getter returns `T | undefined` -- no exceptions on missing keys. `remove()` is the v4 method name (was `delete()` in v3). **Gotcha:** `getString()` on a key stored with `set(key, 42)` returns `undefined`, not `"42"`. MMKV does not auto-convert between types. --- ## Pattern 3: React Hooks for Reactive Storage All hooks accept an optional second argument for a custom MMKV instance. ```typescript import { useMMKVString, useMMKVNumber, useMMKVBoolean, useMMKVBuffer, useMMKVObject, useMMKVKeys, } from "react-native-mmkv"; import { View, Text, Switch, TextInput } from "react-native"; import type { User } from "../types"; function SettingsScreen() { // String hook -- same API as useState const [name, setName] = useMMKVString("user.name"); // Boolean hook -- toggles persist automatically const [darkMode, setDarkMode] = useMMKVBoolean("settings.darkMode"); // Number hook const [fontSize, setFontSize] = useMMKVNumber("settings.fontSize"); // Object hook -- handles JSON serialization internally const [user, setUser] = useMMKVObject<User>("user.profile"); // Keys hook -- reactive list of all keys const allKeys = useMMKVKeys(); return ( <View> <TextInput value={name ?? ""} onChangeText={setName} /> <Switch value={darkMode ?? false} onValueChange={setDarkMode} /> <Text>Stored keys: {allKeys?.length ?? 0}</Text> {user && <Text>Logged in as {user.name}</Text>} </View> ); } export { SettingsScreen }; ``` **Why good:** Hooks trigger re-renders when storage changes (even from other components or native code). `useMMKVObject` handles JSON internally -- no manual stringify/parse. ```typescript // BAD: Manual subscription with useEffect import { useState, useEffect } from "react"; import { storage } from "./storage"; function BadExample() { const [name, setName] = useState(storage.getString("user.name")); useEffect(() => { const listener = storage.addOnValueChangedListener((key) => { if (key === "user.name") setName(storage.getString("user.name")); }); return () => listener.remove(); }, []); return <Text>{name}</Text>; } ``` **Why bad:** Reimplements what `useMMKVString("user.name")` does in one line. Manual listener setup is error-prone and verbose. --- ## Pattern 4: Using Hooks with Custom Instances ```typescript import { useMMKVString, useMMKVObject } from "react-native-mmkv"; import { secureStorage } from "./storage"; import type { AuthToken } from "../types"; function AuthStatus() { // Pass custom instance as second argument const [token, setToken] = useMMKVString("auth.token", secureStorage); const [session, setSession] = useMMKVObject<AuthToken>("auth.session", secureStorage); const handleLogout = () => { setToken(undefined); // Setting undefined removes the key setSession(undefined); }; return token ? <LoggedInView onLogout={handleLogout} /> : <LoginScreen />; } export { AuthStatus }; ``` **Why good:** Encrypted instance isolates sensitive data. Setting `undefined` deletes the key, providing a clean logout. --- ## Pattern 5: Value Change Listeners ### Non-React Listener (services, background tasks) ```typescript import { storage } from "./storage"; // addOnValueChangedListener returns a subscription with .remove() const subscription = storage.addOnValueChangedListener((changedKey) => { switch (changedKey) { case "auth.token": { const token = storage.getString(changedKey); if (!token) { // Token was removed -- handle logout redirectToLogin(); } break; } case "settings.language": { const lang = storage.getString(changedKey); if (lang) updateLocale(lang); break; } } }); // Cleanup when service shuts down subscription.remove(); ``` **Why good:** Works outside React tree, listener receives only the key (read new value yourself), `.remove()` prevents leaks. ### React Hook Listener ```typescript import { useMMKVListener } from "react-native-mmkv"; import { storage } from "./storage"; function AnalyticsTracker() { // useMMKVListener handles cleanup automatically on unmount useMMKVListener((changedKey) => { trackStorageEvent(changedKey); }, storage); // Optional: pass instance, or omit for global listener return null; // Headless component } export { AnalyticsTracker }; ``` **Why good:** Hook handles cleanup on unmount -- no manual `.remove()` call needed. Pass instance as second arg to scope the listener. -
persistence.md 4.3 KB
# MMKV - Persistence Middleware > State management persistence adapter and hydration handling. See [SKILL.md](../SKILL.md) for decision guidance. See [examples/core.md](core.md) for basic MMKV usage. --- ## Pattern 1: StateStorage Adapter Implement a `StateStorage`-compatible interface to bridge MMKV with any persist middleware that accepts `getItem`/`setItem`/`removeItem`. ```typescript // lib/mmkv-state-storage.ts import { createMMKV } from "react-native-mmkv"; const storage = createMMKV(); interface StateStorage { setItem: (name: string, value: string) => void; getItem: (name: string) => string | null; removeItem: (name: string) => void; } export const mmkvStateStorage: StateStorage = { setItem: (name, value) => { storage.set(name, value); }, getItem: (name) => { return storage.getString(name) ?? null; }, removeItem: (name) => { storage.remove(name); }, }; ``` **Why good:** Synchronous adapter -- no Promise wrappers needed. `getItem` returns `null` (not `undefined`) per standard `StateStorage` contract. Works as a drop-in replacement for any AsyncStorage-based adapter. ```typescript // BAD: Async wrapper around synchronous MMKV export const badAdapter = { setItem: async (name: string, value: string) => { storage.set(name, value); }, getItem: async (name: string) => { return storage.getString(name) ?? null; }, removeItem: async (name: string) => { storage.remove(name); }, }; ``` **Why bad:** MMKV is synchronous -- wrapping in `async` adds unnecessary microtask overhead and defeats the performance benefit over AsyncStorage. --- ## Pattern 2: Custom Instance per Store Use separate MMKV instances to encrypt specific stores independently. ```typescript import { createMMKV } from "react-native-mmkv"; interface StateStorage { setItem: (name: string, value: string) => void; getItem: (name: string) => string | null; removeItem: (name: string) => void; } const createMMKVAdapter = ( instanceId: string, encryptionKey?: string, ): StateStorage => { const instance = createMMKV({ id: instanceId, ...(encryptionKey && { encryptionKey, encryptionType: "AES-256" as const }), }); return { setItem: (name, value) => instance.set(name, value), getItem: (name) => instance.getString(name) ?? null, removeItem: (name) => instance.remove(name), }; }; // Unencrypted adapter for preferences export const preferencesAdapter = createMMKVAdapter("preferences"); // Encrypted adapter for auth data export const authAdapter = createMMKVAdapter("auth-secure", "encryption-key"); ``` **Why good:** Each store gets its own MMKV instance with independent encryption. Factory function prevents boilerplate duplication. --- ## Pattern 3: Hydration Handling When using a persist middleware, prevent "flash of initial state" by waiting for hydration before rendering. The specific API depends on your state management solution -- here is the general pattern: ```typescript import { useState, useEffect } from "react"; import { ActivityIndicator, View } from "react-native"; // Assume your persist middleware provides a way to know when hydration is complete. // Common patterns: // - onRehydrateStorage callback that sets a flag // - A hasHydrated() selector on the store // - A Promise that resolves after rehydration function AppRoot({ isHydrated, token }: { isHydrated: boolean; token: string | null }) { if (!isHydrated) { return ( <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}> <ActivityIndicator size="large" /> </View> ); } return token ? <MainNavigator /> : <AuthNavigator />; } export { AppRoot }; ``` **Why good:** Loading screen prevents rendering with stale initial state. Because MMKV is synchronous, hydration is nearly instant -- but the persist middleware may still need a tick to deserialize and apply state. ```typescript // BAD: No hydration check function BadAppRoot({ token }: { token: string | null }) { // token is null initially (default state), then hydrated to real value // User sees login screen flash before main app return token ? <MainNavigator /> : <AuthNavigator />; } ``` **Why bad:** Without hydration check, `token` starts as `null` (initial state) before persist loads the real value from MMKV, causing a visible flash of the auth screen.
-
-
reference.md 7.1 KB
# MMKV Quick Reference > API reference and migration checklist. See [SKILL.md](SKILL.md) for decision framework, red flags, and anti-patterns. --- ## API Reference ### Instance Management | Function | Signature | Returns | | ------------ | --------------------------------------- | ---------------------------- | | `createMMKV` | `(options?: MMKVConfiguration) => MMKV` | MMKV instance | | `existsMMKV` | `(id: string) => boolean` | Whether instance file exists | | `deleteMMKV` | `(id: string) => boolean` | Whether instance was deleted | ### Configuration Options | Option | Type | Default | Purpose | | ------------------ | ------------------------------------- | -------------------- | ----------------------------- | | `id` | `string` | `"mmkv.default"` | Unique instance identifier | | `path` | `string` | `$(Documents)/mmkv/` | Custom file directory | | `encryptionKey` | `string` | `undefined` | Enables AES encryption | | `encryptionType` | `"AES-128" \| "AES-256"` | `"AES-128"` | Encryption strength | | `mode` | `"single-process" \| "multi-process"` | `"single-process"` | Process access mode | | `readOnly` | `boolean` | `false` | Prevent writes | | `compareBeforeSet` | `boolean` | `false` | Skip write if value unchanged | ### Getters and Setters | Method | Signature | Returns | | ------------ | -------------------------------------------------------------------------- | ---------------- | | `set` | `(key: string, value: string \| number \| boolean \| ArrayBuffer) => void` | void | | `getString` | `(key: string) => string \| undefined` | Stored string | | `getNumber` | `(key: string) => number \| undefined` | Stored number | | `getBoolean` | `(key: string) => boolean \| undefined` | Stored boolean | | `getBuffer` | `(key: string) => ArrayBuffer \| undefined` | Stored buffer | | `contains` | `(key: string) => boolean` | Key exists | | `remove` | `(key: string) => void` | Deletes key | | `getAllKeys` | `() => string[]` | All key names | | `clearAll` | `() => void` | Removes all keys | ### Encryption Runtime Methods | Method | Signature | Purpose | | --------- | ------------------------------------------------------ | ------------------------ | | `encrypt` | `(key: string, type?: "AES-128" \| "AES-256") => void` | Enable/change encryption | | `decrypt` | `() => void` | Remove encryption | ### Instance Utilities | Property/Method | Type | Purpose | | --------------- | -------------------------- | ----------------------------------- | | `size` | `number` | Storage size in bytes | | `trim` | `() => void` | Reclaim space from deleted keys | | `importAllFrom` | `(source: MMKV) => number` | Copy all keys from another instance | ### Listeners | Method | Signature | Returns | | --------------------------- | ------------------------------------------------------------- | ------------ | | `addOnValueChangedListener` | `(callback: (key: string) => void) => { remove: () => void }` | Subscription | ### React Hooks | Hook | Signature | Returns | | ------------------ | ----------------------------------------------------------------------------------------------- | ---------------------- | | `useMMKVString` | `(key: string, instance?) => [string \| undefined, (v: string \| undefined) => void]` | Reactive string | | `useMMKVNumber` | `(key: string, instance?) => [number \| undefined, (v: number \| undefined) => void]` | Reactive number | | `useMMKVBoolean` | `(key: string, instance?) => [boolean \| undefined, (v: boolean \| undefined) => void]` | Reactive boolean | | `useMMKVBuffer` | `(key: string, instance?) => [ArrayBuffer \| undefined, (v: ArrayBuffer \| undefined) => void]` | Reactive buffer | | `useMMKVObject<T>` | `(key: string, instance?) => [T \| undefined, (v: T \| undefined) => void]` | Reactive JSON object | | `useMMKVListener` | `(callback: (key: string) => void, instance?) => void` | Auto-cleanup listener | | `useMMKVKeys` | `(instance?) => string[] \| undefined` | Reactive key list | | `useMMKV` | `(options?: MMKVConfiguration) => MMKV` | Reactive MMKV instance | --- ## V3 to V4 Migration | Change | V3 | V4 | | ---------------- | ------------------------ | ---------------------------------- | | Constructor | `new MMKV()` | `createMMKV()` | | Delete key | `storage.delete(key)` | `storage.remove(key)` | | Peer dependency | None | `react-native-nitro-modules` | | Min React Native | 0.71 | 0.75 | | App Group key | `AppGroup` in Info.plist | `AppGroupIdentifier` in Info.plist | | Architecture | JSI TurboModule | Nitro Module | --- ## AsyncStorage Migration Checklist - [ ] Install `react-native-mmkv` and `react-native-nitro-modules` - [ ] Create MMKV instance at module scope - [ ] Write migration function with per-key error handling - [ ] Add migration flag check (`hasMigratedFromAsyncStorage`) - [ ] Wrap migration in `InteractionManager.runAfterInteractions` - [ ] Show loading indicator during migration - [ ] Replace all `await AsyncStorage.getItem()` with `storage.getString()` - [ ] Replace all `await AsyncStorage.setItem()` with `storage.set()` - [ ] Replace all `await AsyncStorage.removeItem()` with `storage.remove()` - [ ] Remove `async`/`await` from storage calls (MMKV is synchronous) - [ ] Update persistence middleware adapter if using a state management persist plugin - [ ] Remove `@react-native-async-storage/async-storage` after migration verified - [ ] Test on both iOS and Android -
SKILL.md 14.4 KB
--- name: mobile-storage-mmkv description: MMKV high-performance key-value storage for React Native - synchronous JSI-based reads/writes, encryption, typed hooks, multiple instances, listeners, persistence middleware adapters --- # MMKV Storage Patterns > **Quick Guide:** Use `createMMKV()` for synchronous key-value storage (~30x faster than AsyncStorage). One singleton instance per concern (global app, per-user). Use typed hooks (`useMMKVString`, `useMMKVObject`) for reactive components. Enable encryption with `encryptionKey` for sensitive data. V4 is a Nitro Module requiring `react-native-nitro-modules` and React Native 0.75+. --- <critical_requirements> ## CRITICAL: Before Using This Skill > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants) **(You MUST reuse a single MMKV instance per concern -- NEVER call `createMMKV()` on every render or in component bodies)** **(You MUST use typed getters (`getString`, `getNumber`, `getBoolean`) -- NEVER parse the return value of the wrong getter)** **(You MUST use `remove()` to delete keys -- `delete()` was renamed in v4 due to C++ keyword conflict)** **(You MUST install `react-native-nitro-modules` alongside `react-native-mmkv` -- v4 is a Nitro Module)** </critical_requirements> --- **Auto-detection:** MMKV, react-native-mmkv, createMMKV, useMMKVString, useMMKVNumber, useMMKVBoolean, useMMKVObject, useMMKVBuffer, useMMKVListener, useMMKVKeys, addOnValueChangedListener, encryptionKey, mmkv storage, key-value storage React Native **When to use:** - Persisting user preferences, auth tokens, or cached data synchronously - Replacing AsyncStorage for faster reads/writes (~30x improvement) - Encrypting sensitive data at rest with AES-128 or AES-256 - Sharing storage between iOS app and extensions via App Groups - Building reactive UIs that re-render on storage changes (hooks) - Isolating data per user or feature with multiple named instances **Key patterns covered:** - Instance creation with `createMMKV()` and configuration options - Typed getters/setters and object serialization - React hooks for reactive storage (`useMMKVString`, `useMMKVObject`, etc.) - Value change listeners (`addOnValueChangedListener`, `useMMKVListener`) - Multiple instances for data isolation (global vs per-user) - Encryption at rest (AES-128/AES-256) - Persistence middleware adapter (generic `StateStorage` interface) - Migration from AsyncStorage **When NOT to use:** - Large binary files or media (use the filesystem) - Relational or queryable data (use a local database) - Data that must sync across devices (use a cloud-synced solution) - Server state caching with invalidation (use your data fetching layer) **Detailed Resources:** - [examples/core.md](examples/core.md) - Instance setup, typed access, hooks, listeners - [examples/advanced.md](examples/advanced.md) - Encryption, multiple instances, App Groups, multi-process, migration - [examples/persistence.md](examples/persistence.md) - State management persistence adapter, hydration handling - [reference.md](reference.md) - API reference, V3-to-V4 migration table, migration checklist --- <philosophy> ## Philosophy MMKV is a **synchronous**, JSI-based key-value store built on top of Tencent's battle-tested C++ library. The key advantage over AsyncStorage is that reads and writes are synchronous -- no `await`, no Promises, no bridge serialization. This eliminates an entire class of race conditions and simplifies code. **Core principles:** 1. **Synchronous by design** -- `getString()` returns immediately, no async wrappers needed 2. **One instance per concern** -- export a singleton; never create instances inside components 3. **Typed access** -- use the correct getter for the stored type; MMKV does not auto-convert 4. **Encrypt sensitive data** -- tokens, keys, PII should use `encryptionKey` option 5. **Hooks for reactivity** -- `useMMKVString` etc. trigger re-renders on changes, replacing manual subscriptions **Performance comparison with AsyncStorage:** | Operation | AsyncStorage | MMKV | Speedup | | -------------- | ------------ | -------- | ------- | | Read 1 key | ~5ms | ~0.015ms | ~300x | | Write 1 key | ~8ms | ~0.018ms | ~440x | | Read 1000 keys | ~200ms | ~3ms | ~65x | Benchmarks vary by device, but MMKV is consistently 30-100x faster for typical operations. **V4 architecture:** MMKV v4 is a Nitro Module (not a TurboModule). This means it uses `react-native-nitro-modules` for the native bridge, requires React Native 0.75+, and the JS API uses `createMMKV()` instead of `new MMKV()`. </philosophy> --- <patterns> ## Core Patterns ### Pattern 1: Instance Creation and Singleton Export Create one instance per storage concern at module scope. Never inside a component or hook body. ```typescript import { createMMKV } from "react-native-mmkv"; // Global app storage -- reuse this everywhere export const storage = createMMKV(); // Named instance for user-specific data export const createUserStorage = (userId: string) => createMMKV({ id: `user-${userId}` }); ``` **Why good:** Module-level creation runs once, all consumers share the same native instance, no wasted allocations ```typescript // BAD: Creating instance inside component function Settings() { const storage = createMMKV(); // New native instance every render // ... } ``` **Why bad:** Creates a new native MMKV instance on every render, wastes memory, defeats instance caching See [examples/core.md](examples/core.md) for full configuration options (path, encryption, readOnly, compareBeforeSet). --- ### Pattern 2: Typed Getters and Setters MMKV stores values by type. Always use the matching getter for what was stored. ```typescript // Set typed values storage.set("user.name", "Alice"); storage.set("user.age", 28); storage.set("onboarded", true); // Get with correct typed getter const name = storage.getString("user.name"); // string | undefined const age = storage.getNumber("user.age"); // number | undefined const done = storage.getBoolean("onboarded"); // boolean | undefined ``` **Why good:** Each getter returns the correct type or `undefined` if key is missing -- no parsing, no type confusion **Gotcha:** Calling `getString` on a key that was stored with `set(key, number)` returns `undefined`, not a stringified number. MMKV does not auto-convert between types. See [examples/core.md](examples/core.md) for object serialization with `JSON.stringify`/`JSON.parse` and `ArrayBuffer` storage. --- ### Pattern 3: React Hooks for Reactive Storage Hooks provide `useState`-like API backed by MMKV. Components re-render when the stored value changes. ```typescript import { useMMKVString, useMMKVBoolean, useMMKVObject, } from "react-native-mmkv"; import type { User } from "../types"; function ProfileScreen() { const [name, setName] = useMMKVString("user.name"); const [darkMode, setDarkMode] = useMMKVBoolean("settings.darkMode"); const [user, setUser] = useMMKVObject<User>("user.profile"); // Set undefined to delete the key const clearProfile = () => setUser(undefined); } ``` **Why good:** Reactive re-renders on change, type-safe generics for objects, setting `undefined` removes the key **Custom instance:** Pass instance as second argument: `useMMKVString("key", userStorage)` See [examples/core.md](examples/core.md) for all hook variants including `useMMKVBuffer` and `useMMKVKeys`. --- ### Pattern 4: Value Change Listeners Listen to storage changes outside React components (background tasks, services, cross-instance sync). ```typescript const listener = storage.addOnValueChangedListener((changedKey) => { const newValue = storage.getString(changedKey); console.log(`${changedKey} changed to: ${newValue}`); }); // Cleanup when no longer needed listener.remove(); ``` **Why good:** Works outside React tree, receives the changed key (read new value yourself), cleanup via `.remove()` For React components, prefer `useMMKVListener` hook -- it handles cleanup automatically. See [examples/core.md](examples/core.md) for `useMMKVListener` hook usage. --- ### Pattern 5: Multiple Instances for Data Isolation Use separate named instances to isolate data by concern. Common pattern: one global instance, one per logged-in user. ```typescript const APP_STORAGE_ID = "app-global"; export const appStorage = createMMKV({ id: APP_STORAGE_ID }); export const createUserStorage = (userId: string) => createMMKV({ id: `user-${userId}` }); // On logout: delete user-specific storage entirely import { deleteMMKV } from "react-native-mmkv"; const handleLogout = (userId: string) => { deleteMMKV(`user-${userId}`); }; ``` **Why good:** User data is fully isolated from app data, `deleteMMKV` removes the entire instance on logout See [examples/advanced.md](examples/advanced.md) for `existsMMKV` checks and instance lifecycle management. --- ### Pattern 6: Encryption Enable AES encryption for sensitive data. Encryption applies to the entire instance -- you cannot encrypt individual keys. ```typescript // Instance with AES-256 encryption const secureStorage = createMMKV({ id: "secure", encryptionKey: "your-encryption-key", encryptionType: "AES-256", }); // Encrypt/decrypt existing instance at runtime storage.encrypt("new-password", "AES-256"); storage.decrypt(); // Remove encryption ``` **When to use:** Auth tokens, API keys, PII, anything that should not be readable if device is compromised See [examples/advanced.md](examples/advanced.md) for key rotation patterns and encryption type comparison. --- ### Pattern 7: Persistence Middleware Adapter Bridge MMKV with state management persistence middleware by implementing a `StateStorage`-compatible interface. ```typescript import { createMMKV } from "react-native-mmkv"; const storage = createMMKV(); // Implement the StateStorage interface your persist middleware expects interface StateStorage { setItem: (name: string, value: string) => void; getItem: (name: string) => string | null; removeItem: (name: string) => void; } export const mmkvStateStorage: StateStorage = { setItem: (name, value) => storage.set(name, value), getItem: (name) => storage.getString(name) ?? null, removeItem: (name) => storage.remove(name), }; ``` **Why good:** Synchronous adapter eliminates async overhead, drop-in replacement for AsyncStorage adapters, works with any persist middleware that accepts `StateStorage` See [examples/persistence.md](examples/persistence.md) for complete persistence middleware setup with hydration handling. </patterns> --- <decision_framework> ## Decision Framework ``` What kind of data are you storing? | +-> Key-value pairs (strings, numbers, booleans, small objects)? | +-> Sensitive data (tokens, keys, PII)? | | +-> YES -> MMKV with encryptionKey | | +-> NO -> MMKV without encryption | +-> Need reactive UI updates? | | +-> YES -> Use MMKV hooks (useMMKVString, etc.) | | +-> NO -> Use direct get/set API | +-> Multiple users or data domains? | +-> YES -> Multiple named instances | +-> NO -> Single default instance | +-> Large files or binary media? | +-> Use the filesystem (not MMKV) | +-> Relational data with queries? | +-> Use a local database (not MMKV) | +-> Server-cached data with invalidation? +-> Use your data fetching layer (not MMKV) ``` ### When to Use Each API Style | Scenario | API | | ------------------------------- | ----------------------------------- | | Read/write in services or utils | Direct: `storage.getString()` | | Reactive component state | Hook: `useMMKVString()` | | Cross-component sync | Hook or `addOnValueChangedListener` | | Background task or service | Direct + listener | | State management persistence | `StateStorage` adapter | </decision_framework> --- <red_flags> ## RED FLAGS **High Priority Issues:** - Calling `createMMKV()` inside a component body -- creates new native instance every render, use module-scope singleton - Using `storage.delete()` -- renamed to `storage.remove()` in v4, `delete` is a C++ reserved keyword - Mixing typed getters -- `getString` on a number key returns `undefined`, not a string. Use the matching getter. - Missing `react-native-nitro-modules` peer dependency -- v4 crashes at runtime without it - Using v4 on React Native < 0.75 -- Nitro Modules require RN 0.75+ **Medium Priority Issues:** - Storing large objects (>1MB) in MMKV -- designed for small key-value pairs, not large blobs - Not calling `listener.remove()` -- native listeners leak if not cleaned up - Using default instance for sensitive data without encryption -- device compromise exposes data - Forgetting `encryptionType: "AES-256"` when AES-256 is needed -- default is AES-128 **Gotchas & Edge Cases:** - MMKV encryption applies to the entire instance, not individual keys -- use a separate encrypted instance for sensitive data - `useMMKVObject<T>` uses `JSON.stringify`/`JSON.parse` internally -- objects with `Date`, `Map`, `Set` lose their types - Setting a hook value to `undefined` deletes the key from storage -- intentional API, not a bug - Remote JS debugging (Chrome DevTools) does not work with MMKV -- JSI requires on-device execution. Use Flipper or React DevTools - `compareBeforeSet` option prevents writing if value is unchanged -- useful for reducing disk I/O in high-frequency updates - iOS App Groups require `AppGroupIdentifier` in Info.plist (was `AppGroup` in v3) and `mode: "multi-process"` - MMKV provides automatic test mocks -- `createMMKV()` works in test runners without native compilation - `getAllKeys()` returns all keys as an array -- there is no prefix filtering, implement it yourself if needed - `storage.size` returns bytes used -- call `storage.trim()` to reclaim space from deleted keys </red_flags> --- <critical_reminders> ## CRITICAL REMINDERS > **All code must follow project conventions in CLAUDE.md** **(You MUST reuse a single MMKV instance per concern -- NEVER call `createMMKV()` on every render or in component bodies)** **(You MUST use typed getters (`getString`, `getNumber`, `getBoolean`) -- NEVER parse the return value of the wrong getter)** **(You MUST use `remove()` to delete keys -- `delete()` was renamed in v4 due to C++ keyword conflict)** **(You MUST install `react-native-nitro-modules` alongside `react-native-mmkv` -- v4 is a Nitro Module)** **Failure to follow these rules will cause memory leaks, runtime crashes, or silent data loss.** </critical_reminders>
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.