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:

src/data/storage/mmkv.ts
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',
});
Store encryption keys securely using expo-secure-store or environment variables!

Basic Usage

Strings

typescript
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

typescript
// Set
storage.set('user.age', 25);

// Get
const age = storage.getNumber('user.age');
console.log(age); // 25

Booleans

typescript
// Set
storage.set('user.verified', true);

// Get
const isVerified = storage.getBoolean('user.verified');
console.log(isVerified); // true

Objects (JSON)

typescript
// 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:

src/data/storage/helpers.ts
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),
};
typescript
// 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:

src/stores/settings.store.ts
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:

src/data/storage/secure.ts
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:

src/data/storage/cache.ts
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:

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

typescript
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

typescript
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

MMKV is synchronous - don't block the main thread with heavy operations!

Debugging

typescript
// Inspect storage in development
if (__DEV__) {
  const keys = storage.getAllKeys();
  console.log('Storage keys:', keys);
  
  keys.forEach((key) => {
    console.log(`${key}:`, storage.getString(key));
  });
}

Next Steps