Skip to main content
The MMKV plugin provides high-performance synchronous storage for React Native using react-native-mmkv. MMKV is up to 30x faster than AsyncStorage.

Installation

npm install @legendapp/state react-native-mmkv
For iOS, install pods:
cd ios && pod install

Setup

import { configureObservableSync } from '@legendapp/state/sync'
import { ObservablePersistMMKV } from '@legendapp/state/persist-plugins/mmkv'

configureObservableSync({
  persist: {
    plugin: ObservablePersistMMKV
  }
})

Configuration

id
string
Unique identifier for the MMKV instance
plugin: new ObservablePersistMMKV({
  id: 'myapp-storage'
})
path
string
Custom file path for the MMKV instance
plugin: new ObservablePersistMMKV({
  path: `${USER_DIRECTORY}/storage`
})
encryptionKey
string
Encryption key for encrypted storage
plugin: new ObservablePersistMMKV({
  id: 'secure-storage',
  encryptionKey: 'my-encryption-key-123'
})

Usage

Basic Usage

import { synced } from '@legendapp/state/sync'

const user$ = synced({
  get: () => api.getUser(),
  persist: {
    name: 'user'
    // Uses globally configured MMKV plugin
  }
})

With Custom Instance

import { ObservablePersistMMKV } from '@legendapp/state/persist-plugins/mmkv'

const user$ = synced({
  persist: {
    name: 'user',
    plugin: new ObservablePersistMMKV({
      id: 'user-storage'
    })
  }
})

Per-Observable MMKV Config

const data$ = synced({
  persist: {
    name: 'data',
    mmkv: {
      id: 'custom-instance',
      encryptionKey: 'secret'
    }
  }
})

Plugin API

The MMKV plugin implements the ObservablePersistPlugin interface:

getTable()

getTable<T>(table: string, init: object, config: PersistOptions): T
Retrieves data from MMKV storage (synchronous).

set()

set(table: string, changes: Change[], config: PersistOptions): void
Saves changes to MMKV storage (synchronous).

getMetadata()

getMetadata(table: string, config: PersistOptions): PersistMetadata
Retrieves sync metadata.

setMetadata()

setMetadata(table: string, metadata: PersistMetadata, config: PersistOptions): void
Saves sync metadata.

deleteTable()

deleteTable(table: string, config: PersistOptions): void
Removes data from MMKV.

deleteMetadata()

deleteMetadata(table: string, config: PersistOptions): void
Removes metadata from MMKV.

Examples

Global Configuration

import { configureObservableSync } from '@legendapp/state/sync'
import { ObservablePersistMMKV } from '@legendapp/state/persist-plugins/mmkv'

configureObservableSync({
  persist: {
    plugin: new ObservablePersistMMKV({
      id: 'app-storage'
    })
  }
})

Encrypted Storage

import { synced } from '@legendapp/state/sync'
import { ObservablePersistMMKV } from '@legendapp/state/persist-plugins/mmkv'

// Secure user data with encryption
const user$ = synced({
  get: () => api.getUser(),
  persist: {
    name: 'user',
    plugin: new ObservablePersistMMKV({
      id: 'secure-user-storage',
      encryptionKey: getEncryptionKey()  // From secure storage
    })
  }
})

Multiple MMKV Instances

import { ObservablePersistMMKV } from '@legendapp/state/persist-plugins/mmkv'

// Regular storage
const settings$ = synced({
  initial: { theme: 'light' },
  persist: {
    name: 'settings',
    mmkv: { id: 'app-storage' }
  }
})

// Encrypted storage for sensitive data
const credentials$ = synced({
  persist: {
    name: 'credentials',
    mmkv: {
      id: 'secure-storage',
      encryptionKey: 'my-secret-key'
    }
  }
})

// Separate instance for cache
const cache$ = synced({
  persist: {
    name: 'cache',
    mmkv: { id: 'cache-storage' }
  }
})

App Settings

import { synced } from '@legendapp/state/sync'
import { observer } from '@legendapp/state/react'

const settings$ = synced({
  initial: {
    theme: 'light',
    notifications: true,
    fontSize: 14
  },
  persist: { name: 'app-settings' }
})

const SettingsScreen = observer(function SettingsScreen() {
  return (
    <View>
      <Switch
        value={settings$.theme.get() === 'dark'}
        onValueChange={(dark) => 
          settings$.theme.set(dark ? 'dark' : 'light')
        }
      />
      <Text>Dark Mode</Text>
      
      <Slider
        value={settings$.fontSize.get()}
        onValueChange={(size) => settings$.fontSize.set(size)}
        minimumValue={12}
        maximumValue={24}
      />
      <Text>Font Size</Text>
    </View>
  )
})

User Session

import { synced } from '@legendapp/state/sync'
import { when } from '@legendapp/state'

interface User {
  id: string
  name: string
  email: string
  token: string
}

const user$ = synced<User | null>({
  get: async () => {
    const token = user$.token.get()
    if (!token) return null
    return api.getCurrentUser(token)
  },
  persist: { name: 'user' }
})

// Wait for user to load
await when(() => !!user$.get())

if (user$.get()) {
  navigateToHome()
} else {
  navigateToLogin()
}

// Logout
function logout() {
  user$.set(null)
}

Offline-First Todo App

import { synced } from '@legendapp/state/sync'
import { observer } from '@legendapp/state/react'

interface Todo {
  id: string
  title: string
  done: boolean
}

const todos$ = synced<Todo[]>({
  get: () => api.getTodos(),
  set: ({ value }) => api.saveTodos(value),
  initial: [],
  persist: {
    name: 'todos',
    retrySync: true  // Retry failed syncs
  }
})

const TodoList = observer(function TodoList() {
  const todos = todos$.get()
  
  return (
    <FlatList
      data={todos}
      renderItem={({ item, index }) => (
        <View>
          <Text>{item.title}</Text>
          <Switch
            value={item.done}
            onValueChange={(done) => 
              todos$[index].done.set(done)
            }
          />
        </View>
      )}
    />
  )
})

function addTodo(title: string) {
  todos$.push({
    id: Date.now().toString(),
    title,
    done: false
  })
}

Clear Storage

import { syncState } from '@legendapp/state/sync'

const user$ = synced({
  persist: { name: 'user' }
})

const settings$ = synced({
  persist: { name: 'settings' }
})

async function clearAllData() {
  await Promise.all([
    syncState(user$).resetPersistence(),
    syncState(settings$).resetPersistence()
  ])
}

// Or clear specific instance
import { MMKV } from 'react-native-mmkv'
const storage = new MMKV({ id: 'app-storage' })
storage.clearAll()

MMKV v3 vs v4 Support

The plugin automatically detects and supports both MMKV v3 and v4:
// v3 (deprecated)
const storage = new MMKV({ id: 'storage' })
storage.delete('key')  // v3 API

// v4 (current)
import { createMMKV } from 'react-native-mmkv'
const storage = createMMKV({ id: 'storage' })
storage.remove('key')  // v4 API

// Plugin works with both automatically

Performance

MMKV is significantly faster than AsyncStorage:
OperationAsyncStorageMMKV
Read~2-3ms~0.1ms
Write~5-10ms~0.2ms
Speedup-~30x
// AsyncStorage (async, slow)
import AsyncStorage from '@react-native-async-storage/async-storage'
const value = await AsyncStorage.getItem('key')  // ~3ms

// MMKV (sync, fast)
import { MMKV } from 'react-native-mmkv'
const storage = new MMKV()
const value = storage.getString('key')  // ~0.1ms

Encryption

MMKV supports AES-256 encryption:
import { ObservablePersistMMKV } from '@legendapp/state/persist-plugins/mmkv'
import * as Keychain from 'react-native-keychain'

// Get encryption key from keychain
const credentials = await Keychain.getGenericPassword()
const encryptionKey = credentials.password

const securePlugin = new ObservablePersistMMKV({
  id: 'secure-storage',
  encryptionKey
})

const sensitiveData$ = synced({
  persist: {
    name: 'sensitive',
    plugin: securePlugin
  }
})

Storage Size

MMKV can handle large datasets efficiently:
// Works well even with large data
const largeData$ = synced({
  initial: {
    items: Array.from({ length: 10000 }, (_, i) => ({
      id: i,
      data: 'large string...'
    }))
  },
  persist: { name: 'large-data' }
})

// Still fast to read/write individual items
largeData$.items[5000].data.set('updated')

Best Practices

  1. Use globally: Configure MMKV once at app startup
  2. Encrypt sensitive data: Use encryption for credentials, tokens, etc.
  3. Separate instances: Use different instances for different data types
  4. Monitor size: MMKV is fast but still has storage limits
  5. Clear on logout: Remove sensitive data when user logs out

Migration from AsyncStorage

import AsyncStorage from '@react-native-async-storage/async-storage'
import { MMKV } from 'react-native-mmkv'

const storage = new MMKV({ id: 'app-storage' })

// Migrate existing data
const keys = await AsyncStorage.getAllKeys()
const items = await AsyncStorage.multiGet(keys)

for (const [key, value] of items) {
  if (value) {
    storage.set(key, value)
  }
}

// Clear old AsyncStorage
await AsyncStorage.clear()

// Update plugin
import { ObservablePersistMMKV } from '@legendapp/state/persist-plugins/mmkv'

configureObservableSync({
  persist: {
    plugin: new ObservablePersistMMKV({ id: 'app-storage' })
  }
})

Debugging

MMKV provides helpful debugging methods:
import { MMKV } from 'react-native-mmkv'

const storage = new MMKV({ id: 'app-storage' })

// List all keys
console.log('Keys:', storage.getAllKeys())

// Check if key exists
if (storage.contains('user')) {
  console.log('User data exists')
}

// Get storage size
const size = storage.size
console.log(`Storage size: ${size} bytes`)

// Clear all data
storage.clearAll()

See Also

Build docs developers (and LLMs) love