react-native-mmkv and react-native-sqlite-storage represent two fundamentally different approaches to local data persistence in React Native. react-native-mmkv is a high-performance, synchronous key-value storage engine built on top of MMKV (Memory-Mapped Key-Value), optimized for speed and simplicity when storing primitive data types, JSON objects, or small binary blobs. It replaces the older AsyncStorage with a significantly faster, synchronous API that blocks the JS thread briefly but completes operations so quickly it feels instant. react-native-sqlite-storage, on the other hand, provides a bridge to SQLite, a full-featured relational database engine. It allows developers to execute SQL queries, define schemas, create relationships between tables, and perform complex filtering, sorting, and aggregation directly on the device. While powerful, it operates asynchronously and requires a deeper understanding of database design principles.
When building React Native apps, deciding how to store data locally is a critical architectural choice. react-native-mmkv and react-native-sqlite-storage solve this problem in very different ways. One prioritizes raw speed and simplicity for key-value pairs, while the other offers the full power of a relational database. Let's break down how they handle real-world scenarios.
react-native-mmkv is designed for speed. It uses memory-mapped files, which means reading and writing data happens synchronously on the JavaScript thread. Because the operations are so fast (often sub-millisecond), they don't cause noticeable UI freezes.
import { MMKV } from 'react-native-mmkv';
const storage = new MMKV();
// Synchronous write - returns immediately
storage.set('user-token', 'abc-123-xyz');
// Synchronous read - returns value immediately
const token = storage.getString('user-token');
console.log(token); // 'abc-123-xyz'
react-native-sqlite-storage operates asynchronously. Every database interaction requires a callback or a Promise because SQLite queries can take time, especially with large datasets. This prevents blocking the UI but adds complexity to your code flow.
import * as SQLite from 'react-native-sqlite-storage';
const db = SQLite.openDatabase({ name: 'mydb.db' });
db.transaction(tx => {
// Asynchronous write - requires callback
tx.executeSql(
'INSERT INTO tokens (key, value) VALUES (?, ?)',
['user-token', 'abc-123-xyz'],
() => console.log('Token saved'),
(error) => console.error('Save failed', error)
);
});
// Asynchronous read - requires callback
let token;
db.transaction(tx => {
tx.executeSql(
'SELECT value FROM tokens WHERE key = ?',
['user-token'],
(tx, results) => {
if (results.rows.length > 0) {
token = results.rows.item(0).value;
console.log(token);
}
}
);
});
react-native-mmkv treats data as simple key-value pairs. You can store strings, numbers, booleans, or byte arrays. For complex objects, you must serialize them to JSON yourself. There is no concept of tables, columns, or relationships.
import { MMKV } from 'react-native-mmkv';
const storage = new MMKV();
// Storing a complex object requires manual serialization
const user = { id: 1, name: 'Alice', preferences: { theme: 'dark' } };
storage.set('user-profile', JSON.stringify(user));
// Retrieving requires manual parsing
const raw = storage.getString('user-profile');
const parsedUser = raw ? JSON.parse(raw) : null;
react-native-sqlite-storage requires you to define a schema with tables and columns. This structure enforces data types and allows you to store related data in separate tables with clear relationships.
import * as SQLite from 'react-native-sqlite-storage';
const db = SQLite.openDatabase({ name: 'mydb.db' });
// Define schema with tables and relationships
db.transaction(tx => {
tx.executeSql(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE
)
`);
tx.executeSql(`
CREATE TABLE IF NOT EXISTS preferences (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
theme TEXT,
FOREIGN KEY (user_id) REFERENCES users(id)
)
`);
});
// Insert structured data without manual serialization
db.transaction(tx => {
tx.executeSql(
'INSERT INTO users (name, email) VALUES (?, ?)',
['Alice', 'alice@example.com']
);
});
react-native-mmkv has no querying language. To find data, you must know the exact key. You can list all keys and filter them manually in JavaScript, but this loads everything into memory first, which is inefficient for large datasets.
import { MMKV } from 'react-native-mmkv';
const storage = new MMKV();
// Get value by exact key only
const value = storage.getString('specific-key');
// Manual filtering (inefficient for large data)
const allKeys = storage.getAllKeys();
const userKeys = allKeys.filter(key => key.startsWith('user-'));
const users = userKeys
.map(key => JSON.parse(storage.getString(key) || '{}'))
.filter(user => user.active === true);
react-native-sqlite-storage gives you the full power of SQL. You can filter, sort, join tables, aggregate data, and use complex conditions directly in the database engine, which is highly optimized for these operations.
import * as SQLite from 'react-native-sqlite-storage';
const db = SQLite.openDatabase({ name: 'mydb.db' });
// Complex query with JOIN, WHERE, and ORDER BY
db.transaction(tx => {
tx.executeSql(
`SELECT u.name, p.theme
FROM users u
JOIN preferences p ON u.id = p.user_id
WHERE u.active = ?
ORDER BY u.name ASC`,
[1],
(tx, results) => {
const activeUsers = [];
for (let i = 0; i < results.rows.length; i++) {
activeUsers.push(results.rows.item(i));
}
console.log(activeUsers);
}
);
});
react-native-mmkv does not support transactions. Each write operation is atomic at the individual key level, but you cannot group multiple writes together to ensure they all succeed or all fail. If your app crashes midway through updating several keys, you could end up with inconsistent data.
import { MMKV } from 'react-native-mmkv';
const storage = new MMKV();
// No transaction support - if crash happens after first set, data is inconsistent
storage.set('account-balance', 1000);
storage.set('last-updated', new Date().toISOString());
// If app crashes here, balance is updated but timestamp is not
react-native-sqlite-storage supports full ACID transactions. You can wrap multiple operations in a transaction block, ensuring that either all changes are applied or none are, maintaining data integrity even if the app crashes.
import * as SQLite from 'react-native-sqlite-storage';
const db = SQLite.openDatabase({ name: 'mydb.db' });
// Transaction ensures atomicity
db.transaction(tx => {
tx.executeSql('UPDATE accounts SET balance = balance - 100 WHERE id = ?', [1]);
tx.executeSql('UPDATE accounts SET balance = balance + 100 WHERE id = ?', [2]);
// If either fails, both changes are rolled back automatically
}, error => {
console.log('Transaction failed, changes rolled back');
}, () => {
console.log('Transaction succeeded, both changes applied');
});
You need to save theme settings, language preferences, and authentication tokens. Data is small, accessed frequently, and doesn't require relationships.
react-native-mmkv// react-native-mmkv approach
const storage = new MMKV();
storage.set('theme', 'dark');
storage.set('auth-token', getSecureToken());
const currentTheme = storage.getString('theme');
You're building a field service app where technicians view customer records, work orders, and parts inventory offline. Data has relationships and needs complex filtering.
react-native-sqlite-storage// react-native-sqlite-storage approach
db.transaction(tx => {
tx.executeSql(
`SELECT * FROM work_orders
WHERE status = 'pending'
AND technician_id = ?
ORDER BY priority DESC`,
[techId]
);
});
You want to cache API responses to reduce network calls. Keys are URLs, values are JSON responses with timestamps.
react-native-mmkv// react-native-mmkv approach
const cacheKey = `api-${endpoint}`;
const cached = storage.getString(cacheKey);
if (cached) {
const { data, timestamp } = JSON.parse(cached);
if (Date.now() - timestamp < MAX_AGE) {
return data;
}
}
You need to track balances, transactions, or stock levels where accuracy and consistency are critical. Partial updates must never happen.
react-native-sqlite-storage// react-native-sqlite-storage approach
db.transaction(tx => {
tx.executeSql('INSERT INTO transactions (amount, type) VALUES (?, ?)', [-50, 'debit']);
tx.executeSql('UPDATE accounts SET balance = balance - 50 WHERE id = ?', [accountId]);
// Both succeed or both fail
});
react-native-sqlite-storage requires careful schema management. As your app evolves, you'll need to handle database migrations (adding columns, changing types) which adds complexity. There is no built-in migration system - you must write version-checking logic yourself.
react-native-mmkv stores everything in a single file per instance. While extremely fast, this means you cannot easily share subsets of data between different parts of your app or enforce access patterns. Also, since it's synchronous, very large JSON strings (megabytes) could cause noticeable pauses.
| Feature | react-native-mmkv | react-native-sqlite-storage |
|---|---|---|
| API Style | Synchronous | Asynchronous |
| Data Model | Key-Value pairs | Relational tables |
| Querying | Key lookup only | Full SQL support |
| Transactions | Not supported | Full ACID support |
| Setup Complexity | Minimal (install and use) | High (schema design, migrations) |
| Best For | Settings, tokens, small caches | Complex datasets, offline apps |
| Performance | Extremely fast for simple ops | Optimized for complex queries |
react-native-mmkv is your go-to for 80% of mobile storage needs. If you're replacing AsyncStorage, storing user preferences, managing session state, or caching small amounts of data, MMKV's speed and simplicity will make your life easier. It removes the async boilerplate and just works.
react-native-sqlite-storage is a specialized tool for when you genuinely need a database. If your app handles hundreds or thousands of related records, requires complex reporting, or must guarantee data consistency through transactions, SQLite is worth the extra complexity. Don't use it just because it feels more "robust" - use it because you need its specific capabilities.
Final Thought: Start with react-native-mmkv for simplicity and speed. Only reach for react-native-sqlite-storage when your data relationships and query needs outgrow what key-value storage can provide. Many successful apps use both - MMKV for settings and session data, SQLite for the main business data.
Choose react-native-mmkv if your primary need is fast, simple storage for user preferences, session tokens, small caches, or serialized JSON objects. It is ideal when you do not need complex querying, relationships, or transactions, and when you want to avoid the boilerplate of defining schemas and writing SQL. Its synchronous API simplifies code flow, making it perfect for settings screens, feature flags, or storing small amounts of structured data where read/write speed is critical.
Choose react-native-sqlite-storage if your application requires storing large datasets with complex relationships, needs to perform advanced queries (JOINs, GROUP BY, complex WHERE clauses), or must enforce data integrity through constraints and transactions. It is the right choice for offline-first apps with substantial local data, reporting tools, or any scenario where data structure and query flexibility outweigh the need for raw read/write speed of simple values.