Implementing Data Persistence
1. Using AsyncStorage API
Example: Basic AsyncStorage Usage
import { AsyncStorage } from 'react-native';
// Save data
await AsyncStorage.setItem('userToken', 'abc123');
// Retrieve data
const token = await AsyncStorage.getItem('userToken');
console.log(token); // "abc123"
| Method | Description | Notes |
|---|---|---|
| setItem(key, value) | Saves a key-value pair | Value must be a string |
| getItem(key) | Retrieves a value by key | Returns a promise resolving to the value |
| removeItem(key) | Deletes a key-value pair | No return value |
| getAllKeys() | Retrieves all keys | Returns a promise with an array of keys |
| multiSet(keyValuePairs) | Sets multiple key-value pairs | Takes an array of [key, value] tuples |
| multiGet(keys) | Retrieves multiple values | Returns a promise with an array of [key, value] tuples |
Note: AsyncStorage is built on top of the underlying platform's storage (e.g., SQLite on Android, NSUserDefaults on iOS). It is not suitable for large data or sensitive information.
2. Storing Complex Data
Example: Saving a User Profile Object
import AsyncStorage from '@react-native-async-storage/async-storage';
const saveUserProfile = async (userProfile) => {
try {
const jsonValue = JSON.stringify(userProfile);
await AsyncStorage.setItem('userProfile', jsonValue);
} catch (e) {
console.error('Failed to save user profile', e);
}
};
// Usage
const userProfile = {
id: '12345',
name: 'John Doe',
preferences: {
theme: 'dark',
notifications: true
}
};
saveUserProfile(userProfile);
| Method | Description | Notes |
|---|---|---|
| AsyncStorage.setItem(key, value) | Saves a key-value pair | Value must be a string |
| AsyncStorage.getItem(key) | Retrieves a value by key | Returns a promise resolving to the string value |
| AsyncStorage.multiSet(pairs) | Sets multiple key-value pairs | Accepts array of [key, value] tuples |
| AsyncStorage.multiGet(keys) | Retrieves multiple values by keys | Returns array of [key, value] tuples |
Note: For complex data structures, always serialize with JSON before storing and deserialize when retrieving.
Example: Loading and Parsing User Preferences
import AsyncStorage from '@react-native-async-storage/async-storage';
const loadUserPreferences = async () => {
try {
const jsonValue = await AsyncStorage.getItem('userPreferences');
if (jsonValue != null) {
const preferences = JSON.parse(jsonValue);
return preferences;
}
} catch (e) {
console.error('Failed to load user preferences', e);
}
return null;
};
// Usage
const prefs = await loadUserPreferences();
console.log(prefs?.theme); // "dark"
Best Practices for Complex Data Storage
- Use JSON.stringify() and JSON.parse() for complex objects
- Wrap storage operations in try-catch blocks
- Consider using a wrapper function to abstract AsyncStorage usage
- Be mindful of storage limits (typically 6MB on mobile)
3. Implementing Data Encryption
| Feature | Description | Implementation |
|---|---|---|
| Encryption Algorithm | Used for securing data at rest | AES-256-GCM |
| Key Management | Secure storage and retrieval of encryption keys | Hardware-backed keystore (Android) or Keychain (iOS) |
| Data Format | Encrypted data structure | {iv: string, data: string} |
Example: Encrypting Data with AES-256-GCM
import { encrypt, decrypt } from 'react-native-crypto'; // hypothetical library
const secretKey = 'your-256-bit-secret-key-here';
const dataToEncrypt = JSON.stringify({ userId: '123', token: 'abc123' });
const encrypted = encrypt(secretKey, dataToEncrypt);
console.log(encrypted); // { iv: '...', data: '...' }
Example: Decrypting Data
const decrypted = decrypt(secretKey, encrypted.iv, encrypted.data);
console.log(JSON.parse(decrypted)); // { userId: '123', token: 'abc123' }
Note: For production apps, use secure key storage like
react-native-keychain or platform-specific secure storage APIs.
Warning: Never hardcode encryption keys in your app. Always use secure key management practices.
Best Practices for Data Encryption
- Use industry-standard algorithms like AES-256-GCM
- Store keys securely using platform-specific secure storage (e.g., Android Keystore, iOS Keychain)
- Never store encryption keys in plain text or local storage
- Implement proper error handling for decryption failures
4. Using MMKV for Performance
Example: Basic MMKV Setup
import MMKV from 'react-native-mmkv';
const storage = new MMKV();
// Set a value
storage.set('user.name', 'John Doe');
// Get a value
const name = storage.getString('user.name');
console.log(name); // John Doe
// Remove a value
storage.delete('user.name');
| Method | Description | Notes |
|---|---|---|
| set(key, value) | Sets a value for the given key | Supports string, number, boolean, and object values |
| getString(key) | Gets a string value for the given key | Returns null if key does not exist |
| getNumber(key) | Gets a number value for the given key | Returns null if key does not exist |
| getBoolean(key) | Gets a boolean value for the given key | Returns null if key does not exist |
| getObject(key) | Gets an object value for the given key | Returns null if key does not exist |
| delete(key) | Deletes a value for the given key | No-op if key does not exist |
| getAllKeys() | Gets all keys in the storage | Array of strings |
| clearAll() | Clears all values in the storage | Removes all keys and their values |
Note: MMKV uses a binary format for storing data, which makes it faster than AsyncStorage.
Warning: MMKV is not encrypted by default. For sensitive data, consider using encryption options provided by the library.
MMKV Performance Benefits
- Fast read/write operations due to binary format
- Memory-efficient storage
- Supports large data sets without performance degradation
- Works well with React Native's native modules
5. Handling Storage Limits
| Storage Type | Limit | Notes |
|---|---|---|
| AsyncStorage | ~6MB |
May vary by platform and device |
| SQLite | Unlimited |
Depends on device storage |
| File System (React Native) | Device storage |
Subject to OS limits and permissions |
Example: Check Storage Usage
import { AsyncStorage } from 'react-native';\n\nconst checkStorageUsage = async () => {\n try {\n const keys = await AsyncStorage.getAllKeys();\n let totalSize = 0;\n for (const key of keys) {\n const value = await AsyncStorage.getItem(key);\n totalSize += new Blob([value]).size;\n }\n console.log(`Total storage used: ${totalSize} bytes`);\n } catch (error) {\n console.error('Error checking storage:', error);\n }\n};
Note: AsyncStorage is not suitable for large data sets due to its size limitations. For larger datasets, consider using SQLite or file system storage.
Warning: Exceeding storage limits can cause app crashes or data loss. Always implement checks and cleanup strategies.
6. Implementing Cache Management
Example: Using AsyncStorage for Caching
import AsyncStorage from '@react-native-async-storage/async-storage';
const cacheKey = 'userProfile';
const userData = {
id: 123,
name: 'John Doe',
email: 'john@example.com'
};
// Save to cache
const saveToCache = async () => {
try {
await AsyncStorage.setItem(cacheKey, JSON.stringify(userData));
console.log('Data cached successfully');
} catch (error) {
console.error('Failed to cache data:', error);
}
};
// Retrieve from cache
const getFromCache = async () => {
try {
const cachedData = await AsyncStorage.getItem(cacheKey);
if (cachedData) {
return JSON.parse(cachedData);
}
return null;
} catch (error) {
console.error('Failed to retrieve from cache:', error);
return null;
}
};
| Method | Description | Notes |
|---|---|---|
| setItem(key, value) | Saves data to storage | Value must be a string |
| getItem(key) | Retrieves data from storage | Returns a promise with the string value |
| removeItem(key) | Deletes a key-value pair | Removes the item from storage |
| getAllKeys() | Retrieves all keys | Useful for cache cleanup |
Note: AsyncStorage is not suitable for large data sets or sensitive information. For larger data, consider using SQLite or Realm.
Example: Cache with Expiration
const CACHE_TIMEOUT = 5 * 60 * 1000; // 5 minutes
const setCachedWithExpiry = async (key, value) => {
const item = {
value: JSON.stringify(value),
timestamp: Date.now()
};
await AsyncStorage.setItem(key, JSON.stringify(item));
};
const getCachedWithExpiry = async (key) => {
const cachedItem = await AsyncStorage.getItem(key);
if (!cachedItem) return null;
const { value, timestamp } = JSON.parse(cachedItem);
if (Date.now() - timestamp > CACHE_TIMEOUT) {
await AsyncStorage.removeItem(key);
return null;
}
return JSON.parse(value);
};
Cache Management Best Practices
- Use
AsyncStoragefor simple key-value caching - Implement expiration logic to prevent stale data
- Avoid caching large data sets or sensitive information
- Consider using
react-native-mmkvfor better performance
7. Using Secure Storage
Example: Storing and Retrieving Secure Data
import * as SecureStore from 'expo-secure-store';
// Store a value
await SecureStore.setItemAsync('authToken', 'abc123xyz');
// Retrieve a value
const token = await SecureStore.getItemAsync('authToken');
console.log(token); // "abc123xyz"
| Method | Description | Notes |
|---|---|---|
| setItemAsync(key, value) | Stores a value securely | Overwrites existing key |
| getItemAsync(key) | Retrieves a value by key | Returns null if key doesn't exist |
| deleteItemAsync(key) | Deletes a stored value | No error if key doesn't exist |
| hasItemAsync(key) | Checks if a key exists | Returns boolean |
Note: SecureStore uses platform-specific secure storage (Keychain on iOS, Keystore on Android). It is encrypted and protected from non-rooted devices.
Warning: Do not store sensitive data in regular AsyncStorage or local storage — use SecureStore for tokens, passwords, or other sensitive values.
8. Migrating Storage Data
Example: Migrating from AsyncStorage to SQLite
import AsyncStorage from '@react-native-async-storage/async-storage';
import SQLite from 'react-native-sqlite-storage';
// Old: AsyncStorage
const oldData = await AsyncStorage.getItem('userPreferences');
// New: SQLite migration
const newData = await new Promise((resolve, reject) => {
SQLite.openDatabase({name: 'app.db'}).transaction(tx => {
tx.executeSql(
'SELECT * FROM userPreferences',
[],
(_, results) => resolve(results.rows),
(_, error) => reject(error)
);
});
});
| Storage Type | Migration Path | Notes |
|---|---|---|
| AsyncStorage | react-native-async-storage |
Legacy, limited performance |
| SQLite | react-native-sqlite-storage |
Structured, scalable |
| Realm | realm |
Object-oriented, real-time sync |
Migration Strategy
- Export data from old storage format
- Transform and validate data structure
- Write to new storage with error handling
- Remove old storage references after success
Warning: Always backup user data before migration to prevent data loss.
Note: Use versioned schemas to handle future data structure changes.
9. Clearing Storage
Example: Clearing AsyncStorage
import AsyncStorage from '@react-native-async-storage/async-storage';
// Clear all storage
await AsyncStorage.clear();
// Clear specific key
await AsyncStorage.removeItem('userToken');
| Method | Description | Notes |
|---|---|---|
| AsyncStorage.clear() | Removes all keys and values | Asynchronous operation |
| AsyncStorage.removeItem(key) | Removes a specific key-value pair | Key must exist or no-op |
Note: Clearing storage is irreversible and should be used carefully in production.
Warning: This action will permanently delete all stored data, including user preferences and session tokens.
10. Implementing Storage Listeners
Example: Setting up a storage listener with Firebase
import { database } from './firebaseConfig';
const ref = database.ref('users');
ref.on('value', (snapshot) => {
const data = snapshot.val();
console.log('User data updated:', data);
});
// To remove the listener later
ref.off('value');
| Method | Description | Parameters | Notes |
|---|---|---|---|
| on(eventType, callback) | Registers a listener for a specific event type | eventType: string, callback: function |
Triggers when data changes at the specified location |
| off(eventType, callback) | Removes a listener for a specific event type | eventType: string, callback: function (optional) |
If no callback is provided, all listeners for that event type are removed |
| once(eventType, callback) | Registers a listener that fires once and then removes itself | eventType: string, callback: function |
Useful for initial data fetches |
Note: Firebase listeners are automatically cleaned up when the component unmounts in React Native apps. It's good practice to explicitly remove listeners to prevent memory leaks.