Claude Skill

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

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download agents-inc-skills-dist_plugins_mobile-storage-mmkv_skills_mobile-storage-mmkv-3a51ef5.zip · 14 KB
Part of agents-inc/skills — 130 skills

Install

skills CLI npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/mobile-storage-mmkv/skills/mobile-storage-mmkv
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
Git 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 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:




<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>

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.

No comments yet.

Reviews (0)

No reviews yet.

Related