Offline Storage in React Native: AsyncStorage vs SQLite
Introduction
Offline storage is crucial for modern mobile applications. Users expect apps to work seamlessly regardless of network connectivity, maintaining data integrity and providing instant access to information. React Native offers several storage solutions, with AsyncStorage and SQLite being the most popular choices for different use cases.
Choosing the right storage solution can significantly impact your app's performance, user experience, and development complexity. While AsyncStorage excels in simplicity and is perfect for storing lightweight data, SQLite provides robust relational database capabilities for complex data structures and queries.
In this comprehensive guide, we'll dive deep into both AsyncStorage and SQLite, comparing their strengths, weaknesses, and ideal use cases to help you make an informed decision for your React Native application.
What You'll Learn
- Understanding AsyncStorage capabilities and limitations
- Exploring SQLite features and implementation in React Native
- Performance comparison between both storage solutions
- Best practices for each storage method
- When to choose AsyncStorage vs SQLite
- Implementation examples and code patterns
Understanding React Native Storage Options
Storage Requirements in Mobile Apps
Modern mobile applications need to handle various types of data storage:
- User Preferences: Settings, theme preferences, language choices
- Authentication Data: Tokens, user sessions, login credentials
- Application State: Current screen, form data, temporary values
- Cache Data: API responses, images, frequently accessed content
- Business Data: User-generated content, transactions, complex relationships
The choice between AsyncStorage and SQLite largely depends on the complexity and volume of data your application needs to handle.
AsyncStorage: Simple Key-Value Storage
What is AsyncStorage?
AsyncStorage is React Native's built-in, asynchronous key-value storage system. It's designed to be simple, persistent, and global to your app. Think of it as a large, persistent hash table that survives app restarts.
Key characteristics:
- Asynchronous: Non-blocking operations using Promises
- Persistent: Data survives app restarts and updates
- Simple: Key-value pairs with string-based storage
- Global: Accessible from anywhere in your app
- Platform-agnostic: Works on both iOS and Android
AsyncStorage Implementation
Basic Setup
First, install AsyncStorage (it was moved out of React Native core):
npm install @react-native-async-storage/async-storage
For iOS, you'll need to run:
cd ios && pod install
Basic Operations
Here's how to perform basic CRUD operations with AsyncStorage:
import AsyncStorage from '@react-native-async-storage/async-storage';
// Store data
const storeData = async (key, value) => {
try {
const jsonValue = JSON.stringify(value);
await AsyncStorage.setItem(key, jsonValue);
console.log('Data stored successfully');
} catch (error) {
console.error('Error storing data:', error);
}
};
// Retrieve data
const getData = async (key) => {
try {
const jsonValue = await AsyncStorage.getItem(key);
return jsonValue != null ? JSON.parse(jsonValue) : null;
} catch (error) {
console.error('Error retrieving data:', error);
return null;
}
};
// 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 clearAll = async () => {
try {
await AsyncStorage.clear();
console.log('All data cleared');
} catch (error) {
console.error('Error clearing data:', error);
}
};
Advanced AsyncStorage Patterns
Batch Operations
For better performance when dealing with multiple keys:
// Store multiple items
const storeMultipleItems = async (keyValuePairs) => {
try {
const pairs = keyValuePairs.map(([key, value]) => [
key,
JSON.stringify(value),
]);
await AsyncStorage.multiSet(pairs);
console.log('Multiple items stored');
} catch (error) {
console.error('Error storing multiple items:', error);
}
};
// Get multiple items
const getMultipleItems = async (keys) => {
try {
const values = await AsyncStorage.multiGet(keys);
return values.map(([key, value]) => [
key,
value ? JSON.parse(value) : null,
]);
} catch (error) {
console.error('Error getting multiple items:', error);
return [];
}
};
Storage Helper Class
Create a reusable storage utility:
class StorageManager {
// User preferences
static async setUserPreference(key, value) {
const prefKey = `user_pref_${key}`;
return this.setItem(prefKey, value);
}
static async getUserPreference(key, defaultValue = null) {
const prefKey = `user_pref_${key}`;
const value = await this.getItem(prefKey);
return value !== null ? value : defaultValue;
}
// Cache management with expiration
static async setCacheWithExpiry(key, value, expirationMinutes = 60) {
const expirationTime = Date.now() + expirationMinutes * 60 * 1000;
const cacheData = {
value,
expirationTime,
};
return this.setItem(`cache_${key}`, cacheData);
}
static async getCacheWithExpiry(key) {
const cacheData = await this.getItem(`cache_${key}`);
if (!cacheData) return null;
if (Date.now() > cacheData.expirationTime) {
await this.removeItem(`cache_${key}`);
return null;
}
return cacheData.value;
}
// Generic methods
static async setItem(key, value) {
try {
await AsyncStorage.setItem(key, JSON.stringify(value));
return true;
} catch (error) {
console.error('Storage error:', error);
return false;
}
}
static async getItem(key) {
try {
const value = await AsyncStorage.getItem(key);
return value ? JSON.parse(value) : null;
} catch (error) {
console.error('Storage error:', error);
return null;
}
}
static async removeItem(key) {
try {
await AsyncStorage.removeItem(key);
return true;
} catch (error) {
console.error('Storage error:', error);
return false;
}
}
}
AsyncStorage Pros and Cons
Advantages
✅ Simple API: Easy to learn and implement
✅ Built-in: Part of React Native ecosystem
✅ Asynchronous: Non-blocking operations
✅ Cross-platform: Works on both iOS and Android
✅ Lightweight: Minimal setup required
✅ Fast for Simple Data: Quick access to key-value pairs
Disadvantages
❌ String-only Storage: All data must be serialized to strings
❌ No Querying: Cannot perform complex data queries
❌ No Relationships: No way to establish data relationships
❌ Limited to 6MB: Storage size limitations on some platforms
❌ No Transactions: No atomic operations for multiple changes
❌ Performance: Can be slow with large amounts of data
SQLite: Relational Database Power
What is SQLite?
SQLite is a lightweight, serverless, relational database that runs directly in your application. It provides full SQL capabilities while maintaining a small footprint, making it ideal for mobile applications that need complex data relationships and queries.
Key characteristics:
- Relational: Support for tables, relationships, and complex queries
- ACID Compliant: Atomic, Consistent, Isolated, and Durable transactions
- SQL Support: Full SQL syntax for data manipulation
- Performance: Optimized for read-heavy operations
- Unlimited Size: No practical storage limits
- Cross-platform: Works on all mobile platforms
SQLite Implementation in React Native
Popular SQLite Libraries
- react-native-sqlite-storage: Mature, stable library
- react-native-sqlite-2: Modern alternative with Promise support
- Expo SQLite: For Expo managed workflow
- react-native-quick-sqlite: High-performance option
Setting Up SQLite with react-native-sqlite-storage
Install the library:
npm install react-native-sqlite-storage
For React Native 0.60+, run:
cd ios && pod install
Database Manager Implementation
Create a comprehensive database manager:
import SQLite from 'react-native-sqlite-storage';
// Enable promise API
SQLite.enablePromise(true);
class DatabaseManager {
constructor() {
this.db = null;
}
// Initialize database
async initDatabase() {
try {
this.db = await SQLite.openDatabase({
name: 'AppDatabase.db',
location: 'default',
createFromLocation: '~databases/AppDatabase.db',
});
console.log('Database opened successfully');
await this.createTables();
return this.db;
} catch (error) {
console.error('Database initialization error:', error);
throw error;
}
}
// Create tables
async createTables() {
const createUsersTable = `
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,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`;
const createPostsTable = `
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
title TEXT NOT NULL,
content TEXT,
is_published BOOLEAN DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id)
);
`;
const createIndexes = `
CREATE INDEX IF NOT EXISTS idx_users_email ON users (email);
CREATE INDEX IF NOT EXISTS idx_posts_user_id ON posts (user_id);
`;
try {
await this.db.executeSql(createUsersTable);
await this.db.executeSql(createPostsTable);
await this.db.executeSql(createIndexes);
console.log('Tables created successfully');
} catch (error) {
console.error('Error creating tables:', error);
}
}
// User operations
async createUser(userData) {
const { name, email, age } = userData;
const query = `
INSERT INTO users (name, email, age)
VALUES (?, ?, ?)
`;
try {
const result = await this.db.executeSql(query, [name, email, age]);
return {
success: true,
insertId: result[0].insertId,
};
} catch (error) {
console.error('Error creating user:', error);
return { success: false, error: error.message };
}
}
async getUser(userId) {
const query = 'SELECT * FROM users WHERE id = ?';
try {
const result = await this.db.executeSql(query, [userId]);
if (result[0].rows.length > 0) {
return result[0].rows.item(0);
}
return null;
} catch (error) {
console.error('Error getting user:', error);
return null;
}
}
async updateUser(userId, userData) {
const fields = Object.keys(userData);
const values = Object.values(userData);
const setClause = fields.map((field) => `${field} = ?`).join(', ');
const query = `
UPDATE users
SET ${setClause}, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`;
try {
await this.db.executeSql(query, [...values, userId]);
return { success: true };
} catch (error) {
console.error('Error updating user:', error);
return { success: false, error: error.message };
}
}
async deleteUser(userId) {
// Delete user and their posts (cascade delete)
const deletePostsQuery = 'DELETE FROM posts WHERE user_id = ?';
const deleteUserQuery = 'DELETE FROM users WHERE id = ?';
try {
await this.db.transaction(async (tx) => {
await tx.executeSql(deletePostsQuery, [userId]);
await tx.executeSql(deleteUserQuery, [userId]);
});
return { success: true };
} catch (error) {
console.error('Error deleting user:', error);
return { success: false, error: error.message };
}
}
// Advanced queries
async getUsersWithPosts() {
const query = `
SELECT
u.id as user_id,
u.name,
u.email,
p.id as post_id,
p.title,
p.is_published
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
ORDER BY u.name, p.created_at DESC
`;
try {
const result = await this.db.executeSql(query);
const rows = [];
for (let i = 0; i < result[0].rows.length; i++) {
rows.push(result[0].rows.item(i));
}
return rows;
} catch (error) {
console.error('Error getting users with posts:', error);
return [];
}
}
async searchUsers(searchTerm) {
const query = `
SELECT * FROM users
WHERE name LIKE ? OR email LIKE ?
ORDER BY name
`;
try {
const searchPattern = `%${searchTerm}%`;
const result = await this.db.executeSql(query, [
searchPattern,
searchPattern,
]);
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 searching users:', error);
return [];
}
}
// Database maintenance
async getDatabaseInfo() {
try {
const userCountResult = await this.db.executeSql(
'SELECT COUNT(*) as count FROM users'
);
const postCountResult = await this.db.executeSql(
'SELECT COUNT(*) as count FROM posts'
);
return {
userCount: userCountResult[0].rows.item(0).count,
postCount: postCountResult[0].rows.item(0).count,
};
} catch (error) {
console.error('Error getting database info:', error);
return null;
}
}
async closeDatabase() {
if (this.db) {
await this.db.close();
console.log('Database closed');
}
}
}
// Export singleton instance
export default new DatabaseManager();
Usage in React Native Components
import React, { useState, useEffect } from 'react';
import { View, Text, Button, FlatList, TextInput, Alert } from 'react-native';
import DatabaseManager from './DatabaseManager';
const UserManager = () => {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [newUserName, setNewUserName] = useState('');
const [newUserEmail, setNewUserEmail] = useState('');
useEffect(() => {
initializeDatabase();
}, []);
const initializeDatabase = async () => {
try {
await DatabaseManager.initDatabase();
loadUsers();
} catch (error) {
console.error('Database initialization failed:', error);
setLoading(false);
}
};
const loadUsers = async () => {
setLoading(true);
const usersData = await DatabaseManager.getUsersWithPosts();
setUsers(usersData);
setLoading(false);
};
const addUser = async () => {
if (!newUserName.trim() || !newUserEmail.trim()) {
Alert.alert('Error', 'Please fill in all fields');
return;
}
const result = await DatabaseManager.createUser({
name: newUserName.trim(),
email: newUserEmail.trim(),
age: 25,
});
if (result.success) {
setNewUserName('');
setNewUserEmail('');
loadUsers();
Alert.alert('Success', 'User added successfully');
} else {
Alert.alert('Error', result.error);
}
};
const deleteUser = async (userId) => {
Alert.alert(
'Confirm Delete',
'Are you sure you want to delete this user?',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Delete',
style: 'destructive',
onPress: async () => {
const result = await DatabaseManager.deleteUser(userId);
if (result.success) {
loadUsers();
Alert.alert('Success', 'User deleted successfully');
} else {
Alert.alert('Error', result.error);
}
},
},
]
);
};
const renderUser = ({ item }) => (
<View style={{ padding: 10, borderBottomWidth: 1, borderColor: '#ccc' }}>
<Text style={{ fontSize: 16, fontWeight: 'bold' }}>{item.name}</Text>
<Text>{item.email}</Text>
<Button
title="Delete"
onPress={() => deleteUser(item.user_id)}
color="red"
/>
</View>
);
if (loading) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Loading...</Text>
</View>
);
}
return (
<View style={{ flex: 1, padding: 20 }}>
<Text style={{ fontSize: 24, marginBottom: 20 }}>User Management</Text>
<TextInput
placeholder="Name"
value={newUserName}
onChangeText={setNewUserName}
style={{
borderWidth: 1,
borderColor: '#ccc',
padding: 10,
marginBottom: 10,
}}
/>
<TextInput
placeholder="Email"
value={newUserEmail}
onChangeText={setNewUserEmail}
keyboardType="email-address"
style={{
borderWidth: 1,
borderColor: '#ccc',
padding: 10,
marginBottom: 10,
}}
/>
<Button title="Add User" onPress={addUser} />
<FlatList
data={users}
renderItem={renderUser}
keyExtractor={(item) => item.user_id?.toString()}
style={{ marginTop: 20 }}
/>
</View>
);
};
export default UserManager;
SQLite Pros and Cons
Advantages
✅ Relational Data: Support for complex relationships and joins
✅ SQL Queries: Powerful query capabilities with sorting, filtering
✅ ACID Transactions: Data integrity with atomic operations
✅ Performance: Optimized for large datasets and complex queries
✅ No Size Limits: Handle gigabytes of data efficiently
✅ Data Types: Support for various data types (INTEGER, TEXT, BLOB, etc.)
✅ Indexes: Optimize query performance with indexes
Disadvantages
❌ Complexity: Steeper learning curve and setup
❌ SQL Knowledge: Requires SQL expertise
❌ Overhead: More resource-intensive for simple data
❌ Migration Management: Schema changes need careful handling
❌ Third-party Dependency: Requires additional libraries
Performance Comparison
Benchmark Results
Here's a performance comparison based on common operations:
Small Data (< 1MB)
- AsyncStorage: ⚡ Faster for simple key-value operations
- SQLite: 🐌 Slower due to database overhead
Medium Data (1MB - 10MB)
- AsyncStorage: 🐌 Performance degrades with data size
- SQLite: ⚡ Consistent performance with proper indexing
Large Data (> 10MB)
- AsyncStorage: 🚫 Significant performance issues
- SQLite: ⚡ Excellent performance with optimized queries
Complex Queries
- AsyncStorage: 🚫 Not supported (requires loading all data)
- SQLite: ⚡ Optimized SQL queries with indexes
Real-World Performance Test
// Performance testing utility
class PerformanceTest {
static async compareStorage(dataSize) {
const testData = this.generateTestData(dataSize);
console.log(`Testing with ${dataSize} records`);
// AsyncStorage test
const asyncStart = Date.now();
await AsyncStorage.setItem('test_data', JSON.stringify(testData));
const storedData = await AsyncStorage.getItem('test_data');
const parsedData = JSON.parse(storedData);
const asyncEnd = Date.now();
console.log(`AsyncStorage: ${asyncEnd - asyncStart}ms`);
// SQLite test
const sqliteStart = Date.now();
await DatabaseManager.initDatabase();
for (const record of testData) {
await DatabaseManager.createUser(record);
}
const users = await DatabaseManager.searchUsers('test');
const sqliteEnd = Date.now();
console.log(`SQLite: ${sqliteEnd - sqliteStart}ms`);
return {
asyncStorage: asyncEnd - asyncStart,
sqlite: sqliteEnd - sqliteStart,
};
}
static generateTestData(size) {
return Array.from({ length: size }, (_, i) => ({
name: `Test User ${i}`,
email: `test${i}@example.com`,
age: 20 + (i % 50),
}));
}
}
When to Use Each Solution
Choose AsyncStorage When:
✅ Simple Data: Storing user preferences, settings, tokens
✅ Small Datasets: Less than 1MB of total data
✅ Quick Prototyping: Need fast development with minimal setup
✅ Key-Value Access: Data accessed by unique keys
✅ No Relationships: Independent data without connections
Common AsyncStorage Use Cases:
- User authentication tokens
- App settings and preferences
- Cached API responses
- Form data persistence
- User onboarding progress
- Theme and language preferences
Choose SQLite When:
✅ Complex Data: Relational data with connections
✅ Large Datasets: Multiple megabytes of data
✅ Complex Queries: Need filtering, sorting, joining data
✅ Data Integrity: Require transactions and ACID properties
✅ Reporting: Generate analytics and reports
✅ Offline-First: Complete offline functionality
Common SQLite Use Cases:
- E-commerce product catalogs
- Chat applications with message history
- Task management with categories and tags
- Financial applications with transactions
- Content management systems
- Social media feeds with relationships
Best Practices and Optimization
AsyncStorage Best Practices
1. Data Structure Design
// ❌ Bad: Storing large arrays directly
const badPractice = {
all_users: [
/* thousands of user objects */
],
};
// ✅ Good: Break down data into smaller chunks
const goodPractice = {
user_1: { name: 'John', email: 'john@example.com' },
user_2: { name: 'Jane', email: 'jane@example.com' },
user_index: ['user_1', 'user_2'], // Keep an index
};
2. Error Handling and Fallbacks
class AsyncStorageHelper {
static async safeGet(key, defaultValue = null) {
try {
const value = await AsyncStorage.getItem(key);
return value ? JSON.parse(value) : defaultValue;
} catch (error) {
console.error(`AsyncStorage get error for key ${key}:`, error);
return defaultValue;
}
}
static async safeSet(key, value) {
try {
await AsyncStorage.setItem(key, JSON.stringify(value));
return true;
} catch (error) {
console.error(`AsyncStorage set error for key ${key}:`, error);
// Handle storage quota exceeded
if (error.message.includes('QuotaExceededError')) {
await this.clearOldCache();
// Retry once
try {
await AsyncStorage.setItem(key, JSON.stringify(value));
return true;
} catch (retryError) {
console.error('AsyncStorage retry failed:', retryError);
return false;
}
}
return false;
}
}
static async clearOldCache() {
try {
const keys = await AsyncStorage.getAllKeys();
const cacheKeys = keys.filter((key) => key.startsWith('cache_'));
// Remove old cache entries
for (const key of cacheKeys) {
const data = await this.safeGet(key);
if (data && data.expirationTime < Date.now()) {
await AsyncStorage.removeItem(key);
}
}
} catch (error) {
console.error('Cache cleanup error:', error);
}
}
}
3. Performance Optimization
class OptimizedAsyncStorage {
constructor() {
this.cache = new Map(); // In-memory cache
this.debounceTimers = new Map(); // Debounce frequent writes
}
async get(key) {
// Return from cache if available
if (this.cache.has(key)) {
return this.cache.get(key);
}
const value = await AsyncStorageHelper.safeGet(key);
this.cache.set(key, value);
return value;
}
async set(key, value, debounceMs = 0) {
// Update cache immediately
this.cache.set(key, value);
if (debounceMs > 0) {
// Debounce writes to storage
if (this.debounceTimers.has(key)) {
clearTimeout(this.debounceTimers.get(key));
}
const timer = setTimeout(async () => {
await AsyncStorageHelper.safeSet(key, value);
this.debounceTimers.delete(key);
}, debounceMs);
this.debounceTimers.set(key, timer);
} else {
await AsyncStorageHelper.safeSet(key, value);
}
}
invalidateCache(key) {
this.cache.delete(key);
}
clearCache() {
this.cache.clear();
}
}
SQLite Best Practices
1. Database Schema Design
// Well-designed schema with proper relationships and indexes
const createTables = async () => {
const queries = [
// Users table with constraints
`CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT email_format CHECK (email LIKE '%@%.%')
);`,
// Posts table with foreign key
`CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
title TEXT NOT NULL,
content TEXT,
status TEXT DEFAULT 'draft' CHECK (status IN ('draft', 'published')),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);`,
// Indexes for better performance
`CREATE INDEX IF NOT EXISTS idx_users_email ON users (email);`,
`CREATE INDEX IF NOT EXISTS idx_posts_user_status ON posts (user_id, status);`,
`CREATE INDEX IF NOT EXISTS idx_posts_created_at ON posts (created_at);`,
];
for (const query of queries) {
await db.executeSql(query);
}
};
2. Query Optimization
class OptimizedQueries {
// Use prepared statements and proper indexing
static async getUserPosts(userId, limit = 10, offset = 0) {
const query = `
SELECT
p.id,
p.title,
p.content,
p.created_at,
u.name as author_name
FROM posts p
INNER JOIN users u ON p.user_id = u.id
WHERE p.user_id = ?
ORDER BY p.created_at DESC
LIMIT ? OFFSET ?
`;
return await db.executeSql(query, [userId, limit, offset]);
}
// Batch operations for better performance
static async bulkInsertPosts(posts) {
await db.transaction(async (tx) => {
const query = `
INSERT INTO posts (user_id, title, content, status)
VALUES (?, ?, ?, ?)
`;
for (const post of posts) {
await tx.executeSql(query, [
post.user_id,
post.title,
post.content,
post.status || 'draft',
]);
}
});
}
// Efficient search with full-text search
static async searchPosts(searchTerm) {
// Create FTS virtual table for better search performance
const createFTSTable = `
CREATE VIRTUAL TABLE IF NOT EXISTS posts_fts
USING fts5(title, content, content=posts, content_rowid=id);
`;
await db.executeSql(createFTSTable);
const query = `
SELECT
p.id,
p.title,
p.content,
p.created_at,
snippet(posts_fts, 0, '<mark>', '</mark>', '...', 32) as title_snippet
FROM posts_fts
INNER JOIN posts p ON posts_fts.rowid = p.id
WHERE posts_fts MATCH ?
ORDER BY rank
`;
return await db.executeSql(query, [searchTerm]);
}
}
3. Migration Management
class DatabaseMigration {
constructor() {
this.migrations = [
{
version: 1,
up: async (db) => {
await db.executeSql(`
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT,
email TEXT
);
`);
},
},
{
version: 2,
up: async (db) => {
await db.executeSql(`
ALTER TABLE users ADD COLUMN created_at DATETIME;
`);
await db.executeSql(`
UPDATE users SET created_at = CURRENT_TIMESTAMP;
`);
},
},
{
version: 3,
up: async (db) => {
await db.executeSql(`
CREATE INDEX idx_users_email ON users (email);
`);
},
},
];
}
async getCurrentVersion(db) {
try {
const result = await db.executeSql('PRAGMA user_version;');
return result[0].rows.item(0).user_version;
} catch (error) {
return 0;
}
}
async setVersion(db, version) {
await db.executeSql(`PRAGMA user_version = ${version};`);
}
async runMigrations(db) {
const currentVersion = await this.getCurrentVersion(db);
console.log(`Current database version: ${currentVersion}`);
const pendingMigrations = this.migrations.filter(
(migration) => migration.version > currentVersion
);
if (pendingMigrations.length === 0) {
console.log('Database is up to date');
return;
}
await db.transaction(async (tx) => {
for (const migration of pendingMigrations) {
console.log(`Running migration ${migration.version}`);
await migration.up(tx);
await this.setVersion(tx, migration.version);
}
});
console.log(
`Database migrated to version ${
pendingMigrations[pendingMigrations.length - 1].version
}`
);
}
}
Hybrid Approach: Using Both Together
Sometimes the best solution is using both AsyncStorage and SQLite together, leveraging each for their strengths:
class HybridStorageManager {
constructor() {
this.cache = new OptimizedAsyncStorage();
this.db = DatabaseManager;
}
async initialize() {
await this.db.initDatabase();
await this.loadCriticalDataToCache();
}
// Use AsyncStorage for frequently accessed, small data
async getUserSettings(userId) {
const cacheKey = `user_settings_${userId}`;
let settings = await this.cache.get(cacheKey);
if (!settings) {
// Fallback to database
settings = await this.db.getUserSettings(userId);
if (settings) {
// Cache for future access
await this.cache.set(cacheKey, settings, 1000); // Debounce 1s
}
}
return settings || this.getDefaultSettings();
}
async updateUserSettings(userId, settings) {
// Update cache immediately for responsive UI
const cacheKey = `user_settings_${userId}`;
await this.cache.set(cacheKey, settings);
// Update database in background
setTimeout(async () => {
await this.db.updateUserSettings(userId, settings);
}, 0);
}
// Use SQLite for complex queries and relationships
async getUserFeed(userId, page = 0, pageSize = 20) {
const cacheKey = `user_feed_${userId}_${page}`;
let feed = await this.cache.get(cacheKey);
if (!feed) {
feed = await this.db.getUserFeed(userId, page, pageSize);
// Cache for 5 minutes
await this.cache.setWithExpiry(cacheKey, feed, 5);
}
return feed;
}
// Critical data cached at app startup
async loadCriticalDataToCache() {
const currentUser = await this.db.getCurrentUser();
if (currentUser) {
await this.cache.set('current_user', currentUser);
const settings = await this.db.getUserSettings(currentUser.id);
await this.cache.set(`user_settings_${currentUser.id}`, settings);
}
}
// Sync changes when going online
async syncPendingChanges() {
const pendingChanges = (await this.cache.get('pending_changes')) || [];
for (const change of pendingChanges) {
try {
await this.db.applyChange(change);
console.log(`Synced change: ${change.id}`);
} catch (error) {
console.error(`Failed to sync change ${change.id}:`, error);
}
}
// Clear synced changes
await this.cache.set('pending_changes', []);
}
getDefaultSettings() {
return {
theme: 'light',
notifications: true,
language: 'en',
};
}
}
Decision Framework
Questions to Ask
- Data Complexity: Do you need relationships between different data types?
- Data Volume: How much data will you be storing?
- Query Requirements: Do you need to search, filter, or sort data?
- Performance Needs: What are your speed requirements for data operations?
- Offline Requirements: How robust does your offline functionality need to be?
- Team Expertise: What's your team's familiarity with SQL?
- Development Timeline: How quickly do you need to implement storage?
Decision Matrix
| Factor | AsyncStorage | SQLite | Winner | | ------------------------ | ------------ | ---------- | ------------ | | Simple Implementation | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | AsyncStorage | | Complex Queries | ❌ | ⭐⭐⭐⭐⭐ | SQLite | | Large Data Sets | ⭐⭐ | ⭐⭐⭐⭐⭐ | SQLite | | Data Relationships | ❌ | ⭐⭐⭐⭐⭐ | SQLite | | Performance (Small Data) | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | AsyncStorage | | Performance (Large Data) | ⭐⭐ | ⭐⭐⭐⭐⭐ | SQLite | | Learning Curve | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | AsyncStorage | | Data Integrity | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | SQLite | | Cross-Platform | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | AsyncStorage |
Conclusion
Both AsyncStorage and SQLite have their place in React Native development. The choice ultimately depends on your specific requirements:
Choose AsyncStorage for simple, lightweight data storage needs where you primarily access data by keys. It's perfect for user preferences, tokens, cache data, and small configuration objects. The simplicity and quick implementation make it ideal for rapid prototyping and simple apps.
Choose SQLite when you need robust data management with relationships, complex queries, and large datasets. It's essential for apps that require offline-first capabilities, reporting features, or complex data operations.
Consider a hybrid approach for larger applications where you can leverage both solutions - using AsyncStorage for frequently accessed small data and SQLite for complex business data.
The key is to evaluate your current needs while considering future scalability requirements. Start simple with AsyncStorage if you're unsure, but be prepared to migrate to SQLite as your data complexity grows.
Remember that the best storage solution is the one that meets your specific requirements while maintaining good performance and developer experience. Both options are mature, well-supported, and production-ready for React Native applications.
Key Takeaways
- AsyncStorage: Perfect for simple key-value storage, user preferences, and small datasets
- SQLite: Ideal for complex relational data, large datasets, and robust querying needs
- Performance: AsyncStorage wins for small data, SQLite excels with large datasets
- Complexity: AsyncStorage is simpler to implement, SQLite requires more setup but offers more power
- Hybrid Approach: Using both together can provide the best of both worlds
- Future-Proofing: Consider your long-term data requirements when making the choice
The React Native ecosystem provides excellent tools for both approaches, so you can't go wrong with either choice as long as it aligns with your application's requirements.