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 exportSTORAGE_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 listenersResult 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 failedReact 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: trueListeners
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:
| Feature | MMKV | AsyncStorage |
|---|---|---|
| Read/Write | Synchronous | Asynchronous |
| Performance | ~30x faster | Baseline |
| Encryption | Built-in | Manual |
| Size | No 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.