Storage & Persistence
This boilerplate uses MMKV for high-performance local storage, providing a fast, encrypted alternative to AsyncStorage.
Why MMKV?
⚡ 30x Faster
Much faster than AsyncStorage for both reads and writes
🔒 Encrypted
Built-in encryption for sensitive data
🧠 Synchronous
No async/await needed - simpler code
📦 Small Size
Minimal impact on app bundle size
Setup
MMKV is already configured in the boilerplate:
import { MMKV } from 'react-native-mmkv';
// Default storage instance
export const storage = new MMKV({
id: 'app-storage',
encryptionKey: 'your-encryption-key-here', // Store securely!
});
// Secure storage for sensitive data
export const secureStorage = new MMKV({
id: 'secure-storage',
encryptionKey: 'your-secure-key-here',
});
// Cache storage (no encryption, faster)
export const cacheStorage = new MMKV({
id: 'cache-storage',
});expo-secure-store or environment variables!Basic Usage
Strings
import { storage } from '@/data/storage/mmkv';
// Set
storage.set('user.name', 'John Doe');
// Get
const name = storage.getString('user.name');
console.log(name); // "John Doe"
// Delete
storage.delete('user.name');
// Check if exists
const exists = storage.contains('user.name');Numbers
// Set
storage.set('user.age', 25);
// Get
const age = storage.getNumber('user.age');
console.log(age); // 25Booleans
// Set
storage.set('user.verified', true);
// Get
const isVerified = storage.getBoolean('user.verified');
console.log(isVerified); // trueObjects (JSON)
// Set
const user = { id: 1, name: 'John', email: 'john@example.com' };
storage.set('user', JSON.stringify(user));
// Get
const storedUser = storage.getString('user');
const parsedUser = storedUser ? JSON.parse(storedUser) : null;
console.log(parsedUser.name); // "John"Storage Helpers
Create typed helpers for better DX:
import { storage } from './mmkv';
export const StorageKeys = {
AUTH_TOKEN: 'auth.token',
USER_ID: 'auth.userId',
THEME: 'settings.theme',
LANGUAGE: 'settings.language',
ONBOARDING_COMPLETED: 'app.onboardingCompleted',
} as const;
// Generic get/set with type safety
export function getItem<T>(key: string): T | null {
const value = storage.getString(key);
if (!value) return null;
try {
return JSON.parse(value) as T;
} catch {
return null;
}
}
export function setItem<T>(key: string, value: T): void {
storage.set(key, JSON.stringify(value));
}
export function removeItem(key: string): void {
storage.delete(key);
}
// Specific helpers
export const authStorage = {
getToken: () => storage.getString(StorageKeys.AUTH_TOKEN),
setToken: (token: string) => storage.set(StorageKeys.AUTH_TOKEN, token),
removeToken: () => storage.delete(StorageKeys.AUTH_TOKEN),
getUserId: () => storage.getString(StorageKeys.USER_ID),
setUserId: (id: string) => storage.set(StorageKeys.USER_ID, id),
};
export const settingsStorage = {
getTheme: () => storage.getString(StorageKeys.THEME) as 'light' | 'dark' | null,
setTheme: (theme: 'light' | 'dark') => storage.set(StorageKeys.THEME, theme),
getLanguage: () => storage.getString(StorageKeys.LANGUAGE) ?? 'en',
setLanguage: (lang: string) => storage.set(StorageKeys.LANGUAGE, lang),
};// Usage
import { authStorage, settingsStorage } from '@/data/storage/helpers';
// Clean API
authStorage.setToken('abc123');
const token = authStorage.getToken();
settingsStorage.setTheme('dark');
const theme = settingsStorage.getTheme();Zustand Persistence
MMKV integrates seamlessly with Zustand:
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { storage } from '@/data/storage/mmkv';
interface SettingsState {
theme: 'light' | 'dark';
language: string;
notificationsEnabled: boolean;
setTheme: (theme: 'light' | 'dark') => void;
setLanguage: (language: string) => void;
toggleNotifications: () => void;
}
export const useSettingsStore = create<SettingsState>()(
persist(
(set) => ({
theme: 'light',
language: 'en',
notificationsEnabled: true,
setTheme: (theme) => set({ theme }),
setLanguage: (language) => set({ language }),
toggleNotifications: () =>
set((state) => ({ notificationsEnabled: !state.notificationsEnabled })),
}),
{
name: 'settings-storage',
storage: createJSONStorage(() => ({
getItem: (name) => {
const value = storage.getString(name);
return value ?? null;
},
setItem: (name, value) => {
storage.set(name, value);
},
removeItem: (name) => {
storage.delete(name);
},
})),
}
)
);Secure Storage
For sensitive data like tokens, use encrypted storage:
import { secureStorage } from './mmkv';
export const secureStore = {
// Auth tokens
setAuthToken: (token: string) => {
secureStorage.set('auth.token', token);
},
getAuthToken: (): string | undefined => {
return secureStorage.getString('auth.token');
},
removeAuthToken: () => {
secureStorage.delete('auth.token');
},
// Refresh tokens
setRefreshToken: (token: string) => {
secureStorage.set('auth.refreshToken', token);
},
getRefreshToken: (): string | undefined => {
return secureStorage.getString('auth.refreshToken');
},
// Biometric credentials
setBiometricKey: (key: string) => {
secureStorage.set('biometric.key', key);
},
getBiometricKey: (): string | undefined => {
return secureStorage.getString('biometric.key');
},
// Clear all secure data
clearAll: () => {
secureStorage.clearAll();
},
};Cache Management
Use cache storage for temporary data:
import { cacheStorage } from './mmkv';
interface CacheEntry<T> {
data: T;
timestamp: number;
ttl: number; // Time to live in milliseconds
}
export const cache = {
set: <T>(key: string, data: T, ttl: number = 5 * 60 * 1000) => {
const entry: CacheEntry<T> = {
data,
timestamp: Date.now(),
ttl,
};
cacheStorage.set(key, JSON.stringify(entry));
},
get: <T>(key: string): T | null => {
const raw = cacheStorage.getString(key);
if (!raw) return null;
try {
const entry: CacheEntry<T> = JSON.parse(raw);
const now = Date.now();
// Check if expired
if (now - entry.timestamp > entry.ttl) {
cacheStorage.delete(key);
return null;
}
return entry.data;
} catch {
return null;
}
},
delete: (key: string) => {
cacheStorage.delete(key);
},
clear: () => {
cacheStorage.clearAll();
},
// Clear expired entries
cleanup: () => {
const keys = cacheStorage.getAllKeys();
const now = Date.now();
keys.forEach((key) => {
const raw = cacheStorage.getString(key);
if (!raw) return;
try {
const entry: CacheEntry<unknown> = JSON.parse(raw);
if (now - entry.timestamp > entry.ttl) {
cacheStorage.delete(key);
}
} catch {
// Invalid entry, delete it
cacheStorage.delete(key);
}
});
},
};
// Usage
cache.set('user.profile', userData, 10 * 60 * 1000); // 10 minutes
const profile = cache.get<User>('user.profile');Storage Listeners
Listen to storage changes:
import { storage } from '@/data/storage/mmkv';
import { useEffect, useState } from 'react';
// Listen for changes
const listener = storage.addOnValueChangedListener((key) => {
console.log(`Value for "${key}" changed`);
});
// Remove listener when done
listener.remove();
// React hook
export function useStorageValue(key: string) {
const [value, setValue] = useState(() => storage.getString(key));
useEffect(() => {
const listener = storage.addOnValueChangedListener((changedKey) => {
if (changedKey === key) {
setValue(storage.getString(key));
}
});
return () => listener.remove();
}, [key]);
return value;
}Migration
Migrate data from AsyncStorage to MMKV:
import AsyncStorage from '@react-native-async-storage/async-storage';
import { storage } from '@/data/storage/mmkv';
export async function migrateFromAsyncStorage() {
try {
const keys = await AsyncStorage.getAllKeys();
for (const key of keys) {
const value = await AsyncStorage.getItem(key);
if (value) {
storage.set(key, value);
}
}
// Clear AsyncStorage after migration
await AsyncStorage.clear();
console.log('Migration completed');
} catch (error) {
console.error('Migration failed:', error);
}
}
// Run on app startup
useEffect(() => {
const hasRunMigration = storage.getBoolean('migration.completed');
if (!hasRunMigration) {
migrateFromAsyncStorage().then(() => {
storage.set('migration.completed', true);
});
}
}, []);Storage Utilities
import { storage } from '@/data/storage/mmkv';
export const storageUtils = {
// Get all keys
getAllKeys: () => {
return storage.getAllKeys();
},
// Get storage size
getSize: () => {
const keys = storage.getAllKeys();
let size = 0;
keys.forEach((key) => {
const value = storage.getString(key);
if (value) {
size += value.length;
}
});
return size; // bytes
},
// Clear all data
clearAll: () => {
storage.clearAll();
},
// Export data (for debugging)
exportData: () => {
const keys = storage.getAllKeys();
const data: Record<string, unknown> = {};
keys.forEach((key) => {
data[key] = storage.getString(key);
});
return data;
},
// Import data
importData: (data: Record<string, string>) => {
Object.entries(data).forEach(([key, value]) => {
storage.set(key, value);
});
},
};Best Practices
✅ Use constants for keys
Define storage keys in one place to avoid typos
✅ Encrypt sensitive data
Use encrypted MMKV instance for tokens and credentials
✅ Clean up cache regularly
Remove expired cache entries to save space
✅ Use proper data types
Use getNumber(), getBoolean() instead of parsing strings
✅ Don't store large objects
For large data, consider SQLite or Realm instead
Debugging
// Inspect storage in development
if (__DEV__) {
const keys = storage.getAllKeys();
console.log('Storage keys:', keys);
keys.forEach((key) => {
console.log(`${key}:`, storage.getString(key));
});
}