Introduction
Data persistence is crucial for creating meaningful mobile applications. Users expect their data to be saved between app sessions, whether it’s user preferences, cached data, or application state. React Native provides several options for local storage, each suited for different use cases and data types.
In this chapter, we’ll explore various storage solutions available in React Native, from simple key-value storage to complex database operations. We’ll cover when to use each approach and provide practical examples to help you implement data persistence in your applications.
Storage Options Overview
React Native offers several storage mechanisms:
- AsyncStorage – Simple key-value storage
- React Native MMKV – Fast key-value storage
- SQLite – Relational database storage
- Realm – Object database
- SecureStore – Secure storage for sensitive data
- File System – Direct file operations
AsyncStorage
AsyncStorage is React Native’s built-in asynchronous storage system. It’s perfect for storing simple key-value pairs like user preferences, settings, or small amounts of data.
Installation
For React Native 0.60+, AsyncStorage is a separate package:
npm install @react-native-async-storage/async-storage
For iOS, you’ll need to run:
cd ios && pod install
Basic AsyncStorage Operations
import AsyncStorage from '@react-native-async-storage/async-storage';
// Store data
const storeData = async (key, value) => {
 try {
  await AsyncStorage.setItem(key, value);
  console.log('Data stored successfully');
 } catch (error) {
  console.error('Error storing data:', error);
 }
};
// Retrieve data
const getData = async (key) => {
 try {
  const value = await AsyncStorage.getItem(key);
  if (value !== null) {
   return value;
  }
 } catch (error) {
  console.error('Error retrieving data:', error);
 }
};
// Remove data
const removeData = async (key) => {
 try {
  await AsyncStorage.removeItem(key);
  console.log('Data removed successfully');
 } catch (error) {
  console.error('Error removing data:', error);
 }
};
// Clear all data
const clearAllData = async () => {
 try {
  await AsyncStorage.clear();
  console.log('All data cleared');
 } catch (error) {
  console.error('Error clearing data:', error);
 }
};
Working with Objects
AsyncStorage only stores strings, so you need to serialize objects:
import AsyncStorage from '@react-native-async-storage/async-storage';
// Store object
const storeObject = async (key, object) => {
 try {
  const jsonValue = JSON.stringify(object);
  await AsyncStorage.setItem(key, jsonValue);
 } catch (error) {
  console.error('Error storing object:', error);
 }
};
// Retrieve object
const getObject = async (key) => {
 try {
  const jsonValue = await AsyncStorage.getItem(key);
  return jsonValue != null ? JSON.parse(jsonValue) : null;
 } catch (error) {
  console.error('Error retrieving object:', error);
  return null;
 }
};
// Example usage
const user = {
 id: 1,
 name: 'John Doe',
 email: 'john@example.com',
 preferences: {
  theme: 'dark',
  notifications: true
 }
};
// Store user object
await storeObject('user', user);
// Retrieve user object
const retrievedUser = await getObject('user');
console.log(retrievedUser);
AsyncStorage Hook
Create a custom hook for easier AsyncStorage usage:
import { useState, useEffect } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
const useAsyncStorage = (key, initialValue = null) => {
 const [storedValue, setStoredValue] = useState(initialValue);
 const [loading, setLoading] = useState(true);
 useEffect(() => {
  const loadStoredValue = async () => {
   try {
    const item = await AsyncStorage.getItem(key);
    if (item) {
     setStoredValue(JSON.parse(item));
    }
   } catch (error) {
    console.error('Error loading stored value:', error);
   } finally {
    setLoading(false);
   }
  };
  loadStoredValue();
 }, [key]);
 const setValue = async (value) => {
  try {
   setStoredValue(value);
   await AsyncStorage.setItem(key, JSON.stringify(value));
  } catch (error) {
   console.error('Error setting value:', error);
  }
 };
 const removeValue = async () => {
  try {
   setStoredValue(initialValue);
   await AsyncStorage.removeItem(key);
  } catch (error) {
   console.error('Error removing value:', error);
  }
 };
 return [storedValue, setValue, removeValue, loading];
};
// Usage example
const SettingsScreen = () => {
 const [settings, setSettings, removeSettings, loading] = useAsyncStorage('settings', {
  theme: 'light',
  notifications: true
 });
 if (loading) {
  return <Text>Loading...</Text>;
 }
 const toggleTheme = () => {
  setSettings({
   ...settings,
   theme: settings.theme === 'light' ? 'dark' : 'light'
  });
 };
 return (
  <View>
   <Text>Current theme: {settings.theme}</Text>
   <Button title="Toggle Theme" onPress={toggleTheme} />
  </View>
 );
};
React Native MMKV
MMKV is a high-performance alternative to AsyncStorage, offering better performance and additional features.
Installation
npm install react-native-mmkv
For iOS:
cd ios && pod install
Basic MMKV Usage
import { MMKV } from 'react-native-mmkv';
// Create storage instance
const storage = new MMKV();
// Store data
storage.set('username', 'john_doe');
storage.set('age', 25);
storage.set('isActive', true);
// Retrieve data
const username = storage.getString('username');
const age = storage.getNumber('age');
const isActive = storage.getBoolean('isActive');
// Store objects
const user = { id: 1, name: 'John', email: 'john@example.com' };
storage.set('user', JSON.stringify(user));
// Retrieve objects
const storedUser = JSON.parse(storage.getString('user') || '{}');
// Delete data
storage.delete('username');
// Check if key exists
const hasUsername = storage.contains('username');
// Get all keys
const allKeys = storage.getAllKeys();
// Clear all data
storage.clearAll();
MMKV with Encryption
import { MMKV } from 'react-native-mmkv';
// Create encrypted storage
const encryptedStorage = new MMKV({
 id: 'secure-storage',
 encryptionKey: 'your-encryption-key-here'
});
// Use same methods as regular MMKV
encryptedStorage.set('sensitiveData', 'secret information');
const sensitiveData = encryptedStorage.getString('sensitiveData');
SQLite Database
For complex data structures and relationships, SQLite is the go-to solution.
Installation
npm install react-native-sqlite-storage
Basic SQLite Setup
import SQLite from 'react-native-sqlite-storage';
// Enable debugging
SQLite.DEBUG(true);
SQLite.enablePromise(true);
class DatabaseManager {
 constructor() {
  this.db = null;
 }
 async openDatabase() {
  try {
   this.db = await SQLite.openDatabase({
    name: 'MyDatabase.db',
    location: 'default',
   });
   console.log('Database opened successfully');
   await this.createTables();
  } catch (error) {
   console.error('Error opening database:', error);
  }
 }
 async createTables() {
  try {
   await this.db.executeSql(`
    CREATE TABLE IF NOT EXISTS users (
     id INTEGER PRIMARY KEY AUTOINCREMENT,
     name TEXT NOT NULL,
     email TEXT UNIQUE NOT NULL,
     age INTEGER,
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP
    )
   `);
  Â
   await this.db.executeSql(`
    CREATE TABLE IF NOT EXISTS posts (
     id INTEGER PRIMARY KEY AUTOINCREMENT,
     title TEXT NOT NULL,
     content TEXT,
     user_id INTEGER,
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
     FOREIGN KEY (user_id) REFERENCES users (id)
    )
   `);
  Â
   console.log('Tables created successfully');
  } catch (error) {
   console.error('Error creating tables:', error);
  }
 }
 async insertUser(name, email, age) {
  try {
   const result = await this.db.executeSql(
    'INSERT INTO users (name, email, age) VALUES (?, ?, ?)',
    [name, email, age]
   );
   return result[0].insertId;
  } catch (error) {
   console.error('Error inserting user:', error);
   throw error;
  }
 }
 async getUsers() {
  try {
   const result = await this.db.executeSql('SELECT * FROM users');
   const users = [];
   for (let i = 0; i < result[0].rows.length; i++) {
    users.push(result[0].rows.item(i));
   }
   return users;
  } catch (error) {
   console.error('Error fetching users:', error);
   return [];
  }
 }
 async updateUser(id, name, email, age) {
  try {
   await this.db.executeSql(
    'UPDATE users SET name = ?, email = ?, age = ? WHERE id = ?',
    [name, email, age, id]
   );
  } catch (error) {
   console.error('Error updating user:', error);
   throw error;
  }
 }
 async deleteUser(id) {
  try {
   await this.db.executeSql('DELETE FROM users WHERE id = ?', [id]);
  } catch (error) {
   console.error('Error deleting user:', error);
   throw error;
  }
 }
 async getUserWithPosts(userId) {
  try {
   const result = await this.db.executeSql(`
    SELECT
     u.id as user_id,
     u.name,
     u.email,
     u.age,
     p.id as post_id,
     p.title,
     p.content,
     p.created_at as post_created_at
    FROM users u
    LEFT JOIN posts p ON u.id = p.user_id
    WHERE u.id = ?
   `, [userId]);
  Â
   const rows = result[0].rows;
   if (rows.length === 0) return null;
  Â
   const user = {
    id: rows.item(0).user_id,
    name: rows.item(0).name,
    email: rows.item(0).email,
    age: rows.item(0).age,
    posts: []
   };
  Â
   for (let i = 0; i < rows.length; i++) {
    const row = rows.item(i);
    if (row.post_id) {
     user.posts.push({
      id: row.post_id,
      title: row.title,
      content: row.content,
      created_at: row.post_created_at
     });
    }
   }
  Â
   return user;
  } catch (error) {
   console.error('Error fetching user with posts:', error);
   return null;
  }
 }
 async closeDatabase() {
  if (this.db) {
   await this.db.close();
  }
 }
}
// Usage
const dbManager = new DatabaseManager();
// In your app initialization
useEffect(() => {
 const initDatabase = async () => {
  await dbManager.openDatabase();
 };
Â
 initDatabase();
Â
 return () => {
  dbManager.closeDatabase();
 };
}, []);
Secure Storage
For sensitive data like authentication tokens, use secure storage solutions.
Using Keychain Services (iOS) and Keystore (Android)
npm install react-native-keychain
import * as Keychain from 'react-native-keychain';
// Store credentials
const storeCredentials = async (username, password) => {
 try {
  await Keychain.setInternetCredentials('myapp', username, password);
  console.log('Credentials stored successfully');
 } catch (error) {
  console.error('Error storing credentials:', error);
 }
};
// Retrieve credentials
const getCredentials = async () => {
 try {
  const credentials = await Keychain.getInternetCredentials('myapp');
  if (credentials) {
   return {
    username: credentials.username,
    password: credentials.password
   };
  }
 } catch (error) {
  console.error('Error retrieving credentials:', error);
 }
 return null;
};
// Remove credentials
const removeCredentials = async () => {
 try {
  await Keychain.resetInternetCredentials('myapp');
  console.log('Credentials removed successfully');
 } catch (error) {
  console.error('Error removing credentials:', error);
 }
};
// Store generic data securely
const storeSecureData = async (key, data) => {
 try {
  await Keychain.setInternetCredentials(key, 'data', JSON.stringify(data));
 } catch (error) {
  console.error('Error storing secure data:', error);
 }
};
const getSecureData = async (key) => {
 try {
  const credentials = await Keychain.getInternetCredentials(key);
  if (credentials) {
   return JSON.parse(credentials.password);
  }
 } catch (error) {
  console.error('Error retrieving secure data:', error);
 }
 return null;
};
File System Operations
For direct file operations, use the React Native File System library.
Installation
npm install react-native-fs
Basic File Operations
import RNFS from 'react-native-fs';
// Get document directory path
const documentsPath = RNFS.DocumentDirectoryPath;
// Write file
const writeFile = async (fileName, content) => {
 try {
  const filePath = `${documentsPath}/${fileName}`;
  await RNFS.writeFile(filePath, content, 'utf8');
  console.log('File written successfully');
 } catch (error) {
  console.error('Error writing file:', error);
 }
};
// Read file
const readFile = async (fileName) => {
 try {
  const filePath = `${documentsPath}/${fileName}`;
  const content = await RNFS.readFile(filePath, 'utf8');
  return content;
 } catch (error) {
  console.error('Error reading file:', error);
  return null;
 }
};
// Check if file exists
const fileExists = async (fileName) => {
 try {
  const filePath = `${documentsPath}/${fileName}`;
  return await RNFS.exists(filePath);
 } catch (error) {
  console.error('Error checking file existence:', error);
  return false;
 }
};
// Delete file
const deleteFile = async (fileName) => {
 try {
  const filePath = `${documentsPath}/${fileName}`;
  await RNFS.unlink(filePath);
  console.log('File deleted successfully');
 } catch (error) {
  console.error('Error deleting file:', error);
 }
};
// List files in directory
const listFiles = async () => {
 try {
  const files = await RNFS.readDir(documentsPath);
  return files.map(file => ({
   name: file.name,
   path: file.path,
   size: file.size,
   isFile: file.isFile(),
   isDirectory: file.isDirectory()
  }));
 } catch (error) {
  console.error('Error listing files:', error);
  return [];
 }
};
// Download file
const downloadFile = async (url, fileName) => {
 try {
  const filePath = `${documentsPath}/${fileName}`;
  const download = RNFS.downloadFile({
   fromUrl: url,
   toFile: filePath,
   progress: (res) => {
    const progress = (res.bytesWritten / res.contentLength) * 100;
    console.log(`Download progress: ${progress.toFixed(2)}%`);
   }
  });
 Â
  const result = await download.promise;
  if (result.statusCode === 200) {
   console.log('File downloaded successfully');
   return filePath;
  }
 } catch (error) {
  console.error('Error downloading file:', error);
 }
 return null;
};
Data Persistence Strategies
Cache Management
class CacheManager {
 constructor() {
  this.cacheKey = 'app_cache';
  this.maxCacheSize = 10 * 1024 * 1024; // 10MB
  this.cacheTimeout = 24 * 60 * 60 * 1000; // 24 hours
 }
 async set(key, data, ttl = this.cacheTimeout) {
  try {
   const cacheItem = {
    data,
    timestamp: Date.now(),
    ttl
   };
  Â
   const cache = await this.getCache();
   cache[key] = cacheItem;
  Â
   await this.saveCache(cache);
   await this.cleanup();
  } catch (error) {
   console.error('Error setting cache:', error);
  }
 }
 async get(key) {
  try {
   const cache = await this.getCache();
   const item = cache[key];
  Â
   if (!item) return null;
  Â
   // Check if expired
   if (Date.now() - item.timestamp > item.ttl) {
    delete cache[key];
    await this.saveCache(cache);
    return null;
   }
  Â
   return item.data;
  } catch (error) {
   console.error('Error getting cache:', error);
   return null;
  }
 }
 async getCache() {
  try {
   const cacheData = await AsyncStorage.getItem(this.cacheKey);
   return cacheData ? JSON.parse(cacheData) : {};
  } catch (error) {
   console.error('Error getting cache:', error);
   return {};
  }
 }
 async saveCache(cache) {
  try {
   await AsyncStorage.setItem(this.cacheKey, JSON.stringify(cache));
  } catch (error) {
   console.error('Error saving cache:', error);
  }
 }
 async cleanup() {
  try {
   const cache = await this.getCache();
   const now = Date.now();
   let hasExpired = false;
  Â
   // Remove expired items
   for (const key in cache) {
    if (now - cache[key].timestamp > cache[key].ttl) {
     delete cache[key];
     hasExpired = true;
    }
   }
  Â
   if (hasExpired) {
    await this.saveCache(cache);
   }
  Â
   // Check cache size and remove oldest items if needed
   const cacheSize = JSON.stringify(cache).length;
   if (cacheSize > this.maxCacheSize) {
    const sortedKeys = Object.keys(cache).sort((a, b) =>
     cache[a].timestamp - cache[b].timestamp
    );
   Â
    while (JSON.stringify(cache).length > this.maxCacheSize && sortedKeys.length > 0) {
     const oldestKey = sortedKeys.shift();
     delete cache[oldestKey];
    }
   Â
    await this.saveCache(cache);
   }
  } catch (error) {
   console.error('Error cleaning cache:', error);
  }
 }
 async clear() {
  try {
   await AsyncStorage.removeItem(this.cacheKey);
  } catch (error) {
   console.error('Error clearing cache:', error);
  }
 }
}
// Usage
const cacheManager = new CacheManager();
// Cache API response
const fetchUserData = async (userId) => {
 const cacheKey = `user_${userId}`;
Â
 // Try to get from cache first
 let userData = await cacheManager.get(cacheKey);
Â
 if (!userData) {
  // Fetch from API
  const response = await fetch(`/api/users/${userId}`);
  userData = await response.json();
 Â
  // Store in cache
  await cacheManager.set(cacheKey, userData);
 }
Â
 return userData;
};
Offline Data Sync
class OfflineDataManager {
 constructor() {
  this.queueKey = 'offline_queue';
  this.dataKey = 'offline_data';
 }
 async addToQueue(action) {
  try {
   const queue = await this.getQueue();
   queue.push({
    id: Date.now(),
    ...action,
    timestamp: Date.now()
   });
  Â
   await AsyncStorage.setItem(this.queueKey, JSON.stringify(queue));
  } catch (error) {
   console.error('Error adding to queue:', error);
  }
 }
 async getQueue() {
  try {
   const queueData = await AsyncStorage.getItem(this.queueKey);
   return queueData ? JSON.parse(queueData) : [];
  } catch (error) {
   console.error('Error getting queue:', error);
   return [];
  }
 }
 async syncWithServer() {
  try {
   const queue = await this.getQueue();
   const processedItems = [];
  Â
   for (const item of queue) {
    try {
     await this.processQueueItem(item);
     processedItems.push(item.id);
    } catch (error) {
     console.error('Error processing queue item:', error);
     // Keep failed items in queue for retry
    }
   }
  Â
   // Remove processed items from queue
   const updatedQueue = queue.filter(item => !processedItems.includes(item.id));
   await AsyncStorage.setItem(this.queueKey, JSON.stringify(updatedQueue));
  Â
   return processedItems.length;
  } catch (error) {
   console.error('Error syncing with server:', error);
   return 0;
  }
 }
 async processQueueItem(item) {
  const { type, data } = item;
 Â
  switch (type) {
   case 'CREATE_USER':
    await fetch('/api/users', {
     method: 'POST',
     headers: { 'Content-Type': 'application/json' },
     body: JSON.stringify(data)
    });
    break;
   Â
   case 'UPDATE_USER':
    await fetch(`/api/users/${data.id}`, {
     method: 'PUT',
     headers: { 'Content-Type': 'application/json' },
     body: JSON.stringify(data)
    });
    break;
   Â
   case 'DELETE_USER':
    await fetch(`/api/users/${data.id}`, {
     method: 'DELETE'
    });
    break;
   Â
   default:
    throw new Error(`Unknown action type: ${type}`);
  }
 }
 async storeOfflineData(key, data) {
  try {
   const offlineData = await this.getOfflineData();
   offlineData[key] = {
    data,
    timestamp: Date.now()
   };
  Â
   await AsyncStorage.setItem(this.dataKey, JSON.stringify(offlineData));
  } catch (error) {
   console.error('Error storing offline data:', error);
  }
 }
 async getOfflineData() {
  try {
   const offlineData = await AsyncStorage.getItem(this.dataKey);
   return offlineData ? JSON.parse(offlineData) : {};
  } catch (error) {
   console.error('Error getting offline data:', error);
   return {};
  }
 }
}
Best Practices
1. Choose the Right Storage Solution
- AsyncStorage: Simple key-value pairs, user preferences, small data
- MMKV: High-performance key-value storage, frequent read/write operations
- SQLite: Complex data structures, relationships, large datasets
- Secure Storage: Sensitive data, authentication tokens, credentials
- File System: Large files, documents, media files
2. Error Handling
Always implement proper error handling for storage operations:
const safeStorageOperation = async (operation) => {
 try {
  return await operation();
 } catch (error) {
  console.error('Storage operation failed:', error);
 Â
  // Handle specific errors
  if (error.message.includes('QuotaExceededError')) {
   // Handle storage quota exceeded
   await clearOldData();
   return await operation(); // Retry
  }
 Â
  // Return default value or handle gracefully
  return null;
 }
};
3. Data Validation
Validate data before storing and after retrieving:
const validateAndStore = async (key, data, schema) => {
 try {
  // Validate data structure
  if (!schema.validate(data)) {
   throw new Error('Data validation failed');
  }
 Â
  await AsyncStorage.setItem(key, JSON.stringify(data));
 } catch (error) {
  console.error('Validation or storage failed:', error);
  throw error;
 }
};
const validateAndRetrieve = async (key, schema, defaultValue = null) => {
 try {
  const data = await AsyncStorage.getItem(key);
  if (!data) return defaultValue;
 Â
  const parsedData = JSON.parse(data);
 Â
  // Validate retrieved data
  if (!schema.validate(parsedData)) {
   console.warn('Retrieved data is invalid, returning default');
   return defaultValue;
  }
 Â
  return parsedData;
 } catch (error) {
  console.error('Retrieval or validation failed:', error);
  return defaultValue;
 }
};
4. Performance Optimization
- Batch operations when possible
- Use appropriate data types
- Implement caching strategies
- Clean up unused data regularly
// Batch operations
const batchAsyncStorageOperations = async (operations) => {
 const promises = operations.map(op => {
  switch (op.type) {
   case 'set':
    return AsyncStorage.setItem(op.key, op.value);
   case 'get':
    return AsyncStorage.getItem(op.key);
   case 'remove':
    return AsyncStorage.removeItem(op.key);
   default:
    return Promise.resolve();
  }
 });
Â
 return await Promise.all(promises);
};
5. Migration Strategies
Handle data structure changes gracefully:
const migrateData = async () => {
 try {
  const version = await AsyncStorage.getItem('data_version');
  const currentVersion = '1.2.0';
 Â
  if (version !== currentVersion) {
   await runMigrations(version, currentVersion);
   await AsyncStorage.setItem('data_version', currentVersion);
  }
 } catch (error) {
  console.error('Migration failed:', error);
 }
};
const runMigrations = async (fromVersion, toVersion) => {
 const migrations = {
  '1.0.0': async () => {
   // Migration from 1.0.0 to 1.1.0
   const oldData = await AsyncStorage.getItem('old_key');
   if (oldData) {
    const newData = transformOldData(JSON.parse(oldData));
    await AsyncStorage.setItem('new_key', JSON.stringify(newData));
    await AsyncStorage.removeItem('old_key');
   }
  },
  '1.1.0': async () => {
   // Migration from 1.1.0 to 1.2.0
   // Add new fields with default values
  }
 };
Â
 // Run migrations in sequence
 for (const [version, migration] of Object.entries(migrations)) {
  if (shouldRunMigration(fromVersion, version, toVersion)) {
   await migration();
  }
 }
};
Summary
Data persistence is a crucial aspect of mobile app development. React Native provides multiple storage options, each with its own strengths:
- AsyncStorage for simple key-value storage
- MMKV for high-performance key-value operations
- SQLite for complex relational data
- Secure Storage for sensitive information
- File System for direct file operations
Choose the appropriate storage solution based on your data structure, performance requirements, and security needs. Always implement proper error handling, data validation, and consider migration strategies for long-term maintenance.
In the next chapter, we’ll explore React Native Performance Optimization techniques to ensure your app runs smoothly even with large amounts of stored data.