Skip to Content
Core ConceptsStorage (MMKV)

Storage (MMKV)

RNCopilot uses react-native-mmkv for fast, synchronous key-value persistence. The storage layer provides typed keys, reactive hooks, and result-based error handling.

Architecture

src/utils/storage/ storage.ts -- Core functions (getItem, setItem, removeItem, etc.) useStorage.ts -- React hooks (useStorage, useStorageBoolean) constants.ts -- STORAGE_KEYS constant types.ts -- TypeScript types index.ts -- Barrel export

STORAGE_KEYS

All storage keys are defined in a single constants file. This prevents typos, enables autocomplete, and makes it easy to see every key in use:

// src/utils/storage/constants.ts export const STORAGE_KEYS = { preferences: { theme: 'user_theme_preference', themePreset: 'user_theme_preset', language: 'user_language', onboardingCompleted: 'onboarding_completed', notificationsEnabled: 'notifications_enabled', }, auth: { lastEmail: 'auth_last_email', }, app: { lastVersion: 'app_last_version', launchCount: 'app_launch_count', }, } as const;

Adding New Keys

When you need a new storage key, add it to the appropriate category in STORAGE_KEYS:

export const STORAGE_KEYS = { preferences: { // ... existing keys fontSize: 'user_font_size', // new key }, // ... other categories } as const;

Never use raw string keys. Always reference STORAGE_KEYS.* to ensure type safety and consistency.

Core Functions

setItem — Write a Value

import { setItem, STORAGE_KEYS } from '@/utils/storage'; // String setItem(STORAGE_KEYS.preferences.language, 'ar'); // Number setItem(STORAGE_KEYS.app.launchCount, 42); // Boolean setItem(STORAGE_KEYS.preferences.onboardingCompleted, true); // Object (auto-serialized to JSON) setItem(STORAGE_KEYS.auth.lastEmail, 'user@example.com');

getItem — Read a Value

import { getItem, STORAGE_KEYS } from '@/utils/storage'; const result = getItem<string>(STORAGE_KEYS.preferences.language); if (result.success && result.data) { console.log('Language:', result.data); // 'ar' } else if (!result.success) { console.error('Read failed:', result.error); }

removeItem — Delete a Value

import { removeItem, STORAGE_KEYS } from '@/utils/storage'; const result = removeItem(STORAGE_KEYS.auth.lastEmail); if (result.success) { // Key removed }

hasItem — Check Existence

import { hasItem, STORAGE_KEYS } from '@/utils/storage'; if (hasItem(STORAGE_KEYS.preferences.onboardingCompleted)) { // Key exists }

clear — Remove All Data

import { clear } from '@/utils/storage'; const result = clear(); // Clears all MMKV data and notifies all listeners

Result Objects

Every storage operation returns a result object instead of throwing exceptions:

interface StorageResult<T> { success: boolean; data?: T; error?: Error; }

Always check .success before accessing .data. This pattern prevents silent failures and makes error handling explicit.

const result = getItem<string>(STORAGE_KEYS.preferences.language); // CORRECT -- check success first if (result.success && result.data) { applyLanguage(result.data); } // WRONG -- accessing data without checking success const language = result.data; // Could be undefined if read failed

React Hooks

useStorage — Reactive Key-Value

The useStorage hook provides a reactive interface to a storage key. The component re-renders when the value changes:

import { useStorage } from '@/utils/storage'; import { STORAGE_KEYS } from '@/utils/storage/constants'; function LanguageSelector() { const { value: language, setValue: setLanguage, removeValue, loading, error, refresh, } = useStorage<string>(STORAGE_KEYS.preferences.language, { defaultValue: 'en', }); if (loading) return <Loading />; return ( <Select value={language ?? 'en'} onChange={setLanguage} options={[ { label: 'English', value: 'en' }, { label: 'Arabic', value: 'ar' }, ]} /> ); }

Hook Options

interface UseStorageOptions<T> { defaultValue?: T; // Fallback when key doesn't exist initializeWithDefault?: boolean; // Write defaultValue to storage if key is empty }

Hook Return Value

interface UseStorageReturn<T> { value: T | null; // Current value (or defaultValue) setValue: (v: T | null) => void; // Write a new value removeValue: () => void; // Delete the key loading: boolean; // True during initial read error: Error | null; // Error from last operation refresh: () => void; // Re-read from storage }

useStorageBoolean — Boolean Toggle

A specialized hook for boolean values that adds a toggle function:

import { useStorageBoolean } from '@/utils/storage'; import { STORAGE_KEYS } from '@/utils/storage/constants'; function NotificationsToggle() { const { value: enabled, toggle, loading, } = useStorageBoolean(STORAGE_KEYS.preferences.notificationsEnabled, { defaultValue: true, }); if (loading) return <Loading />; return ( <Switch value={enabled ?? true} onValueChange={toggle} /> ); }

The toggle function flips the current boolean value and persists it:

// If value is true, toggle() sets it to false (and vice versa) const { value, toggle } = useStorageBoolean(key, { defaultValue: false }); // value: false toggle(); // value: true

Listeners

The storage system supports listeners that fire when a key’s value changes. The useStorage hook uses this internally, but you can also subscribe manually:

import { addListener, STORAGE_KEYS } from '@/utils/storage'; const unsubscribe = addListener<string>( STORAGE_KEYS.preferences.theme, (newValue) => { console.log('Theme changed to:', newValue); } ); // Later, clean up unsubscribe();

Storage in Non-React Code

For code outside React components (services, utilities, initialization), use the core functions directly:

// src/i18n/config.ts import { getItem, STORAGE_KEYS } from '@/utils/storage'; const savedLang = getItem<string>(STORAGE_KEYS.preferences.language); let initialLang = 'en'; if (savedLang.success && savedLang.data) { initialLang = savedLang.data; }
// src/theme/themeManager.ts import { setItem, STORAGE_KEYS } from '@/utils/storage'; export function toggleDarkMode(isDark: boolean) { const mode = isDark ? 'dark' : 'light'; // ... apply theme setItem(STORAGE_KEYS.preferences.theme, mode); }

MMKV vs AsyncStorage

MMKV is significantly faster than AsyncStorage because it is synchronous and backed by memory-mapped files:

FeatureMMKVAsyncStorage
Read/WriteSynchronousAsynchronous
Performance~30x fasterBaseline
EncryptionBuilt-inManual
SizeNo practical limit~6MB default

This synchronous behavior is why storage values can be read during initialization (before any React component mounts), enabling features like persisted theme and language preferences.

Last updated on