Perché lo Storage Offline È Fondamentale nelle App Mobile
Se hai mai aperto un'app in metro e ti sei trovato davanti a una schermata bianca, sai già di cosa sto parlando. I dati non mentono: il 94% degli utenti abbandona un'app che non funziona senza connessione. E nel 2026, con le app mobile che gestiscono sempre più dati locali — dalle preferenze utente ai feed complessi — scegliere la giusta strategia di persistenza non è più opzionale. È una decisione architetturale critica.
Il problema? L'ecosistema React Native offre almeno cinque librerie serie per lo storage locale, ognuna con filosofia, prestazioni e casi d'uso diversi. AsyncStorage, MMKV, Expo SQLite, op-sqlite, WatermelonDB — e poi ci sono pattern come TanStack Query con persistenza che cambiano completamente l'approccio. Orientarsi non è banale.
Allora, facciamo ordine. In questa guida le mettiamo tutte a confronto con benchmark reali, codice pronto all'uso e una mappa decisionale chiara. Niente teoria astratta: solo quello che ti serve per scegliere.
AsyncStorage: il Default che Devi Superare
@react-native-async-storage/async-storage v3.0.2 è la libreria di storage più usata nell'ecosistema React Native, con oltre 2,5 milioni di download settimanali su npm. Funziona su tutte le piattaforme (Android, iOS, macOS, Web) ed è inclusa di default in Expo. La conosci già, probabilmente.
Quando Va Bene AsyncStorage
Per configurazioni semplici — un tema scuro, una lingua preferita, un flag di onboarding completato — AsyncStorage è perfettamente adeguato. Zero configurazione, API familiare, funziona ovunque incluso Expo Go.
import AsyncStorage from '@react-native-async-storage/async-storage';
// Salva una preferenza
await AsyncStorage.setItem('theme', 'dark');
await AsyncStorage.setItem('user_prefs', JSON.stringify({
language: 'it',
notifications: true,
}));
// Leggi
const theme = await AsyncStorage.getItem('theme');
const prefs = JSON.parse(await AsyncStorage.getItem('user_prefs') ?? '{}')
I Limiti Reali di AsyncStorage
Ecco dove le cose si complicano (e dove molti si scottano in produzione):
- 20-30x più lento di MMKV nelle operazioni di lettura e scrittura
- Limite di ~6 MB su iOS prima che le prestazioni degradino seriamente
- Nessuna crittografia: i dati sono salvati in chiaro nel filesystem
- Solo API asincrona: ogni operazione richiede
await, niente accesso sincrono - Nessuna query: niente indici, ordinamento o ricerca — solo chiave-valore
- Blocca il thread JS: operazioni frequenti o grandi possono causare lag nell'UI
Il backend di AsyncStorage è SQLite sia su Android che su iOS, ma l'overhead dell'API asincrona basata su bridge lo rende significativamente più lento delle alternative native. Onestamente, per qualsiasi cosa oltre alle semplici preferenze, è il momento di passare a qualcosa di meglio.
MMKV: Storage Key-Value Ultraveloce
react-native-mmkv v4.3.0 (marzo 2026) è una riscrittura completa come Nitro Module, basata sulla libreria MMKV sviluppata originariamente da WeChat per gestire miliardi di operazioni al giorno. I risultati parlano da soli — e non esagero.
Benchmark: MMKV vs AsyncStorage
Test su iPhone 11 Pro, 1.000 operazioni di lettura:
| Operazione | AsyncStorage | MMKV | Velocità |
|---|---|---|---|
| Lettura | 2,548 ms | 0,520 ms | ~5x più veloce |
| Scrittura | 2,871 ms | 0,570 ms | ~5x più veloce |
In scenari più complessi il vantaggio arriva fino a 20-30x. La ragione è semplice: MMKV usa memory-mapped files con accesso diretto tramite JSI, saltando completamente il bridge e il thread JavaScript per le operazioni native. Questo fa tutta la differenza quando l'app cresce.
Installazione e Configurazione
# Con Expo (consigliato)
npx expo install react-native-mmkv react-native-nitro-modules
npx expo prebuild
# Con React Native CLI
npm install react-native-mmkv react-native-nitro-modules
cd ios && pod install
Requisiti: React Native >= 0.74, Nuova Architettura / TurboModules abilitata.
API Completa con Esempi
import { createMMKV } from 'react-native-mmkv';
// Crea un'istanza con configurazione
const storage = createMMKV({
id: 'app.storage', // ID univoco dell'istanza
encryptionKey: 'la-tua-chiave', // Crittografia AES opzionale
encryptionType: 'AES-256', // AES-128 (default) o AES-256
compareBeforeSet: true, // Salta la scrittura se il valore non cambia (v4.3.0+)
});
// Scrittura — supporta string, number, boolean, ArrayBuffer
storage.set('user.name', 'Marco Rossi');
storage.set('user.age', 32);
storage.set('user.premium', true);
// Lettura tipizzata
const name = storage.getString('user.name'); // string | undefined
const age = storage.getNumber('user.age'); // number | undefined
const premium = storage.getBoolean('user.premium'); // boolean | undefined
// Gestione chiavi
storage.contains('user.name'); // true
storage.getAllKeys(); // ['user.name', 'user.age', 'user.premium']
storage.remove('user.age'); // rimuove una chiave
storage.clearAll(); // pulisce tutto
// Dimensione storage
console.log(storage.size); // dimensione in byte
React Hooks Integrati
MMKV v4 include hook React basati su useSyncExternalStore() che si aggiornano automaticamente quando il valore cambia — anche da altre parti dell'app o da altri processi. Questo è uno degli aspetti che preferisco di questa libreria:
import { useMMKVString, useMMKVNumber, useMMKVBoolean } from 'react-native-mmkv';
function UserProfile() {
const [username, setUsername] = useMMKVString('user.name');
const [age, setAge] = useMMKVNumber('user.age');
const [isPremium, setIsPremium] = useMMKVBoolean('user.premium');
return (
<View>
<Text>{username ?? 'Ospite'}</Text>
<Button
title="Aggiorna nome"
onPress={() => setUsername('Nuovo Nome')}
/>
</View>
);
}
Crittografia AES-256
MMKV supporta crittografia AES-128 e AES-256 sia alla creazione che a runtime:
// Crittografia alla creazione
const secureStorage = createMMKV({
id: 'secure.storage',
encryptionKey: 'chiave-da-keychain',
encryptionType: 'AES-256',
});
// Crittografia a runtime (v4.2.0+)
storage.encrypt('nuova-chiave', 'AES-256');
// Controlla stato
console.log(storage.isEncrypted); // true
// Rimuovi crittografia
storage.decrypt();
Best practice: non hardcodare mai la chiave di crittografia nel codice. Recuperala dal Keychain (iOS) o dal Keystore (Android) usando expo-secure-store o react-native-keychain. Sembra ovvio, ma lo vedo succedere ancora troppo spesso.
Integrazione con Zustand
Uno dei pattern più diffusi nel 2026 è usare MMKV come backend di persistenza per Zustand. Se stai già usando Zustand per gestire lo stato, questa integrazione è quasi obbligata:
import { create } from 'zustand';
import { persist, createJSONStorage, StateStorage } from 'zustand/middleware';
import { createMMKV } from 'react-native-mmkv';
const storage = createMMKV({ id: 'zustand.persist' });
const zustandStorage: StateStorage = {
setItem: (name, value) => { storage.set(name, value); },
getItem: (name) => storage.getString(name) ?? null,
removeItem: (name) => { storage.remove(name); },
};
interface AuthStore {
token: string | null;
setToken: (token: string | null) => void;
}
const useAuthStore = create<AuthStore>()(
persist(
(set) => ({
token: null,
setToken: (token) => set({ token }),
}),
{
name: 'auth-storage',
storage: createJSONStorage(() => zustandStorage),
}
)
);
Integrazione con Jotai
import { atomWithStorage, createJSONStorage } from 'jotai/utils';
import { createMMKV } from 'react-native-mmkv';
const mmkv = createMMKV({ id: 'jotai.persist' });
const mmkvStorage = createJSONStorage<any>(() => ({
getItem: (key: string) => mmkv.getString(key) ?? null,
setItem: (key: string, value: string) => mmkv.set(key, value),
removeItem: (key: string) => mmkv.remove(key),
}));
// Atom persistente — il valore sopravvive al riavvio dell'app
export const themeAtom = atomWithStorage('theme', 'light', mmkvStorage, {
getOnInit: true, // Leggi il valore salvato immediatamente
});
export const onboardingAtom = atomWithStorage('onboarding-done', false, mmkvStorage, {
getOnInit: true,
});
Expo SQLite: Database Relazionale Integrato
Quando i dati hanno relazioni tra loro — utenti, ordini, prodotti, categorie — un semplice key-value store non basta più. Serve un database relazionale vero e proprio. Expo SQLite (parte di SDK 52+) è la soluzione integrata nell'ecosistema Expo, completamente riscritta per la Nuova Architettura.
Perché Expo SQLite nel 2026
- API moderna: completamente basata su Promise e async/await (addio callback)
- Tagged template literals: query SQL con interpolazione sicura dei parametri
- API sincrona: metodi
*Syncper quando serve accesso immediato - SQLCipher: crittografia del database integrata
- libSQL/Turso: sincronizzazione con database remoti (SDK 53+)
- KV-Store: drop-in replacement per AsyncStorage con API sincrona
- DevTools: inspector integrato nel browser per debug
Setup e Configurazione
# Installazione
npx expo install expo-sqlite
Configura le opzioni nel file app.json:
{
"expo": {
"plugins": [
["expo-sqlite", {
"enableFTS": true,
"useSQLCipher": true,
"useLibSQL": false
}]
]
}
}
API Fondamentali
import * as SQLite from 'expo-sqlite';
// Apri il database (asincrono)
const db = await SQLite.openDatabaseAsync('app.db');
// Crea tabelle
await db.execAsync(`
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price REAL NOT NULL,
category TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_products_category ON products(category);
`);
// Inserisci dati — runAsync restituisce { lastInsertRowId, changes }
const result = await db.runAsync(
'INSERT INTO products (name, price, category) VALUES (?, ?, ?)',
['iPhone 16', 1099.99, 'electronics']
);
console.log('ID inserito:', result.lastInsertRowId);
// Query singola riga
const product = await db.getFirstAsync<{
id: number; name: string; price: number;
}>('SELECT * FROM products WHERE id = ?', [result.lastInsertRowId]);
// Query tutte le righe
const electronics = await db.getAllAsync<{
id: number; name: string; price: number;
}>('SELECT * FROM products WHERE category = ?', ['electronics']);
// Iterazione con cursore (per dataset grandi)
for await (const row of db.getEachAsync('SELECT * FROM products')) {
console.log(row.name);
}
Tagged Template Literals (SDK 52+)
Questa è probabilmente la mia API preferita di Expo SQLite — la sintassi è elegante e i parametri vengono automaticamente sanitizzati, quindi niente più SQL injection accidentali:
interface Product {
id: number;
name: string;
price: number;
category: string;
}
// Query con tipo — restituisce Product[]
const minPrice = 50;
const category = 'electronics';
const products = await db.sql<Product>`
SELECT * FROM products
WHERE price > ${minPrice}
AND category = ${category}
`;
// Singolo risultato
const cheapest = await db.sql<Product>`
SELECT * FROM products
ORDER BY price ASC
LIMIT 1
`.first();
// Solo valori (senza nomi colonna)
const names = await db.sql`
SELECT name FROM products
`.values();
Transazioni
// Transazione standard
await db.withTransactionAsync(async () => {
await db.runAsync('UPDATE accounts SET balance = balance - ? WHERE id = ?', [100, 1]);
await db.runAsync('UPDATE accounts SET balance = balance + ? WHERE id = ?', [100, 2]);
// Se qualcosa fallisce, viene fatto rollback automatico
});
// Transazione esclusiva (blocca altri accessi in scrittura)
await db.withExclusiveTransactionAsync(async () => {
// Operazioni critiche qui
});
Provider React e Context
import { SQLiteProvider, useSQLiteContext } from 'expo-sqlite';
async function migrateDb(db: SQLite.SQLiteDatabase) {
await db.execAsync(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL
);
`);
}
// Nel componente root
function App() {
return (
<SQLiteProvider databaseName="app.db" onInit={migrateDb}>
<UserList />
</SQLiteProvider>
);
}
// In qualsiasi componente figlio
function UserList() {
const db = useSQLiteContext();
const [users, setUsers] = useState<User[]>([]);
useEffect(() => {
db.getAllAsync<User>('SELECT * FROM users').then(setUsers);
}, []);
return /* render della lista */;
}
KV-Store: il Sostituto di AsyncStorage
Se vuoi migrare da AsyncStorage con il minimo sforzo, expo-sqlite include un KV-Store con API compatibile e in più offre metodi sincroni. Praticamente è un drop-in replacement:
import Storage from 'expo-sqlite/kv-store';
// API asincrona (stessa di AsyncStorage)
await Storage.setItem('user_token', 'abc123');
const token = await Storage.getItem('user_token');
// API sincrona (esclusiva di expo-sqlite)
Storage.setItemSync('theme', 'dark');
const theme = Storage.getItemSync('theme');
// Operazioni batch
await Storage.multiSet([['key1', 'val1'], ['key2', 'val2']]);
const values = await Storage.multiGet(['key1', 'key2']);
op-sqlite: Prestazioni SQLite al Massimo
op-sqlite v15.2.10 (aprile 2026) è l'alternativa più performante per chi ha bisogno di spremere ogni millisecondo da SQLite. Costruita interamente in C con binding JSI diretti, è pensata per app con dataset grandi e query complesse. Non è per tutti, ma quando serve, serve davvero.
Quando Scegliere op-sqlite rispetto a Expo SQLite
| Caratteristica | op-sqlite | Expo SQLite |
|---|---|---|
| Velocità query grandi (300k+ righe) | Significativamente più veloce | Buona |
| Query piccole (<100 righe) | Comparabile | Comparabile |
| Plugin disponibili | SQLCipher, libSQL, FTS5, Rtree, cr-sqlite, sqlite-vec | SQLCipher, libSQL, FTS, sqlite-vec |
| DevTools integrati | No | Sì (inspector browser) |
| ORM supportati | Drizzle, Kysely | Drizzle, Kysely |
| Expo managed workflow | Config plugin necessario | Integrato nativamente |
| Ricerca vettoriale (AI) | sqlite-vec integrato | sqlite-vec (SDK 53+) |
In sostanza: se usi Expo e le tue query operano su dataset di dimensioni normali, Expo SQLite è la scelta più pratica. Se invece lavori con centinaia di migliaia di righe, fai ricerche full-text intensive o hai bisogno di sqlite-vec per AI on-device, op-sqlite ti dà quel margine di prestazioni in più che può fare la differenza.
import { open } from '@op-engineering/op-sqlite';
// Apri il database
const db = open({ name: 'app.db' });
// Query con risultati tipizzati
const { rows } = db.execute(
'SELECT * FROM products WHERE price > ?',
[50]
);
// Transazione
db.transaction((tx) => {
tx.execute('INSERT INTO products (name, price) VALUES (?, ?)', ['AirPods', 249]);
tx.execute('INSERT INTO products (name, price) VALUES (?, ?)', ['iPad', 799]);
});
// Reactive queries — il callback scatta quando i dati cambiano
const unsubscribe = db.reactiveExecute({
query: 'SELECT * FROM products WHERE category = ?',
arguments: ['electronics'],
fireOn: [{ table: 'products' }],
callback: (response) => {
console.log('Dati aggiornati:', response.rows);
},
});
WatermelonDB: Database per Dataset Grandi con Sync
Quando la tua app gestisce decine di migliaia di record e ha bisogno di sincronizzazione con un backend, WatermelonDB è progettato esattamente per questo scenario. A differenza delle altre soluzioni, WatermelonDB non è solo uno storage: è un vero ORM reattivo con lazy loading e un protocollo di sync integrato.
Detto questo, è anche la soluzione con la curva di apprendimento più ripida. Ne vale la pena? Dipende dal tuo caso d'uso.
Filosofia: Caricamento Lazy, UI Istantanea
Il problema con gli store basati su Redux o sullo stato globale è che caricano tutto in memoria all'avvio. Con dataset grandi (50k+ record) questo significa 2-5 secondi di attesa su dispositivi lenti — un'eternità per l'utente. WatermelonDB ribalta l'approccio:
- Niente viene caricato finché non serve: le query girano direttamente su SQLite in un thread nativo separato
- Completamente reattivo: l'UI si aggiorna automaticamente quando i dati cambiano
- Avvio istantaneo: nessun caricamento iniziale, nessun ritardo percepibile
Definizione dei Modelli
import { Model } from '@nozbe/watermelondb';
import { field, text, date, children, relation } from '@nozbe/watermelondb/decorators';
class Product extends Model {
static table = 'products';
static associations = {
reviews: { type: 'has_many' as const, foreignKey: 'product_id' },
category: { type: 'belongs_to' as const, key: 'category_id' },
};
@text('name') name!: string;
@field('price') price!: number;
@text('description') description!: string;
@date('created_at') createdAt!: Date;
@relation('categories', 'category_id') category: any;
@children('reviews') reviews: any;
}
class Review extends Model {
static table = 'reviews';
@text('body') body!: string;
@field('rating') rating!: number;
@relation('products', 'product_id') product: any;
}
Query e Osservazione Reattiva
import { Q } from '@nozbe/watermelondb';
// Query con filtri
const expensiveProducts = await database
.get<Product>('products')
.query(
Q.where('price', Q.gt(100)),
Q.sortBy('created_at', Q.desc),
Q.take(20)
)
.fetch();
// Osservazione reattiva in un componente React
import { withObservables } from '@nozbe/watermelondb/react';
const ProductCard = ({ product }: { product: Product }) => (
<View>
<Text>{product.name}</Text>
<Text>€{product.price}</Text>
</View>
);
const enhance = withObservables(['product'], ({ product }: { product: Product }) => ({
product: product.observe(),
}));
export default enhance(ProductCard);
Sincronizzazione con il Backend
Questa è la funzionalità killer di WatermelonDB, e il motivo principale per cui lo sceglieresti rispetto alle alternative. Il protocollo di sync è un processo in due fasi — pull (scarica le modifiche dal server) e push (invia le modifiche locali) — con gestione dei conflitti integrata:
import { synchronize } from '@nozbe/watermelondb/sync';
async function syncWithServer() {
await synchronize({
database,
pullChanges: async ({ lastPulledAt, schemaVersion, migration }) => {
const response = await fetch(
`https://api.example.com/sync?last_pulled_at=${lastPulledAt}` +
`&schema_version=${schemaVersion}`
);
if (!response.ok) throw new Error('Sync pull fallito');
const { changes, timestamp } = await response.json();
return { changes, timestamp };
},
pushChanges: async ({ changes, lastPulledAt }) => {
const response = await fetch(
`https://api.example.com/sync?last_pulled_at=${lastPulledAt}`,
{ method: 'POST', body: JSON.stringify(changes) }
);
if (!response.ok) throw new Error('Sync push fallito');
},
migrationsEnabledAtVersion: 1,
});
}
Per dataset molto grandi (50k+ record), WatermelonDB offre anche modalità ottimizzate:
- Replacement Sync: il server invia il dataset completo e l'app sostituisce il DB locale preservando le modifiche non sincronizzate
- Turbo Login: per il primo sync su un database vuoto, il JSON grezzo viene passato direttamente al layer nativo saltando il parsing JS — molto più veloce
TanStack Query + MMKV: Data Fetching Offline-First
Le soluzioni viste finora gestiscono lo storage locale. Ma che succede quando i dati arrivano da un'API e vuoi che siano disponibili anche offline? Il pattern TanStack Query + MMKV persistence è la risposta moderna a questo problema: combina il data fetching intelligente di TanStack Query con la velocità di MMKV per la cache persistente.
È il pattern che uso più spesso nei progetti recenti, e ti spiego perché.
Come Funziona
L'idea è semplice: TanStack Query gestisce il ciclo di vita dei dati (fetch, cache, invalidazione, retry) mentre MMKV salva la cache su disco. Quando l'app si riavvia o perde connessione, i dati sono già disponibili istantaneamente dalla cache locale. Nessuna schermata di caricamento, nessun spinner. L'utente non si accorge nemmeno di essere offline.
Setup Completo Step-by-Step
1. Installa le dipendenze:
npx expo install @tanstack/react-query \
@tanstack/react-query-persist-client \
@tanstack/query-async-storage-persister \
react-native-mmkv react-native-nitro-modules \
@react-native-community/netinfo
2. Crea il persister MMKV:
// lib/query-persister.ts
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
import { createMMKV } from 'react-native-mmkv';
const storage = createMMKV({ id: 'tanstack.cache' });
const clientStorage = {
setItem: (key: string, value: string) => { storage.set(key, value); },
getItem: (key: string) => storage.getString(key) ?? null,
removeItem: (key: string) => { storage.remove(key); },
};
export const queryPersister = createAsyncStoragePersister({
storage: clientStorage,
});
3. Configura il QueryClient per offline-first:
// lib/query-client.ts
import { QueryClient } from '@tanstack/react-query';
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: Infinity, // Non eliminare mai la cache
staleTime: 1000 * 60 * 5, // Dati "freschi" per 5 minuti
networkMode: 'offlineFirst', // Usa la cache prima, poi fetch
retry: 2,
},
mutations: {
networkMode: 'offlineFirst',
},
},
});
4. Configura il monitoraggio della connessione:
// lib/online-manager.ts
import NetInfo from '@react-native-community/netinfo';
import { onlineManager } from '@tanstack/react-query';
onlineManager.setEventListener((setOnline) =>
NetInfo.addEventListener((state) => {
setOnline(Boolean(state.isConnected));
})
);
5. Integra tutto nell'App:
// App.tsx
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
import { queryClient } from './lib/query-client';
import { queryPersister } from './lib/query-persister';
import './lib/online-manager';
export default function App() {
return (
<PersistQueryClientProvider
client={queryClient}
persistOptions={{
persister: queryPersister,
maxAge: Infinity, // Mantieni la cache persistente per sempre
}}
>
<NavigationContainer>
{/* Le tue schermate */}
</NavigationContainer>
</PersistQueryClientProvider>
);
}
6. Usa i dati nei componenti:
import { useQuery } from '@tanstack/react-query';
interface Product {
id: number;
name: string;
price: number;
}
function ProductList() {
const { data, isLoading, isError, dataUpdatedAt } = useQuery<Product[]>({
queryKey: ['products'],
queryFn: async () => {
const res = await fetch('https://api.example.com/products');
if (!res.ok) throw new Error('Errore nel caricamento');
return res.json();
},
});
if (isLoading && !data) return <Text>Caricamento...</Text>;
return (
<View>
{data?.map((p) => (
<Text key={p.id}>{p.name} — €{p.price}</Text>
))}
<Text style={{ color: '#999', fontSize: 12 }}>
Ultimo aggiornamento: {new Date(dataUpdatedAt).toLocaleString('it-IT')}
</Text>
</View>
);
}
Un avvertimento importante: il persister predefinito serializza l'intero stato del QueryClient a ogni modifica. Con molte query attive, questo può significare diversi MB di JSON. Per app complesse, considera di implementare un persister personalizzato che salvi le query individualmente — ti risparmierà problemi di performance su dispositivi meno potenti.
Storage Sicuro: Proteggere i Dati Sensibili
Token di autenticazione, chiavi API, dati biometrici — certi dati non possono essere salvati in chiaro. Punto. Ecco le opzioni per lo storage sicuro nel 2026:
expo-secure-store
La scelta migliore per progetti Expo. Usa il Keychain su iOS e EncryptedSharedPreferences su Android:
import * as SecureStore from 'expo-secure-store';
// Salva un token (crittografato dal sistema operativo)
await SecureStore.setItemAsync('auth_token', 'eyJhbGciOi...');
// Leggi il token
const token = await SecureStore.getItemAsync('auth_token');
// Rimuovi
await SecureStore.deleteItemAsync('auth_token');
MMKV con Crittografia AES-256
Per dati che devono essere sia veloci che crittografati, MMKV con AES-256 è un'ottima alternativa. Il trucco è salvare la chiave nel Keychain di sistema:
import * as SecureStore from 'expo-secure-store';
import { createMMKV } from 'react-native-mmkv';
// Recupera la chiave dal Keychain (o generala al primo avvio)
let encKey = await SecureStore.getItemAsync('mmkv_encryption_key');
if (!encKey) {
encKey = generateRandomKey(); // La tua funzione di generazione
await SecureStore.setItemAsync('mmkv_encryption_key', encKey);
}
// Crea istanza MMKV crittografata
const secureStorage = createMMKV({
id: 'secure.data',
encryptionKey: encKey,
encryptionType: 'AES-256',
});
Raccomandazioni per Tipo di Dato
| Tipo di dato | Soluzione consigliata |
|---|---|
| Token JWT, refresh token | expo-secure-store |
| Chiavi API, segreti | expo-secure-store |
| Dati utente sensibili | MMKV + AES-256 (chiave dal Keychain) |
| Preferenze, impostazioni | MMKV (senza crittografia) |
| Dati relazionali | Expo SQLite + SQLCipher |
| Cache API | TanStack Query + MMKV |
Mappa Decisionale: Quale Storage Scegliere
Ok, abbiamo visto un sacco di opzioni. Ecco la guida pratica per non perdersi:
Usa AsyncStorage quando:
- Stai prototipando o è un progetto molto semplice
- Devi salvare solo poche preferenze (tema, lingua, flag)
- Lavori con Expo Go e non puoi fare prebuild
Usa MMKV quando:
- Hai bisogno di key-value storage veloce (sostituisce AsyncStorage)
- Vuoi accesso sincrono ai dati
- Vuoi persistere lo stato di Zustand o Jotai
- Hai bisogno di crittografia AES dei dati
- Vuoi usare React hooks reattivi per lo storage
Usa Expo SQLite quando:
- I tuoi dati hanno relazioni (tabelle, join, indici)
- Hai bisogno di query complesse con filtri e ordinamento
- Usi Expo e vuoi l'integrazione più semplice possibile
- Hai bisogno di ricerca full-text (FTS)
- Vuoi il KV-Store come drop-in replacement di AsyncStorage
Usa op-sqlite quando:
- Lavori con dataset molto grandi (300k+ righe)
- Hai bisogno di prestazioni SQLite massime
- Usi sqlite-vec per ricerche vettoriali e AI on-device
- Il tuo progetto non è gestito con Expo managed workflow
Usa WatermelonDB quando:
- La tua app ha 10k-100k+ record con sincronizzazione server
- Hai bisogno di un ORM reattivo con lazy loading
- Vuoi un protocollo di sync pull/push integrato
- L'avvio istantaneo è critico anche con grandi dataset
Usa TanStack Query + MMKV quando:
- I dati arrivano da API REST/GraphQL e vuoi cache offline persistente
- Vuoi che l'app funzioni offline con i dati già scaricati
- Hai bisogno di invalidazione intelligente e refetch automatico
Combinare le Soluzioni: l'Architettura Completa
Nella pratica, la maggior parte delle app production-ready usa una combinazione di queste soluzioni. Non devi sceglierne una sola — anzi, non dovresti. Ecco un esempio di architettura completa per un'app e-commerce:
// Architettura storage di un'app e-commerce
// 1. MMKV — preferenze utente e stato UI
const preferences = createMMKV({ id: 'prefs' });
// tema, lingua, filtri salvati, cronologia ricerche
// 2. expo-secure-store — dati sensibili
// token JWT, refresh token, chiavi API
// 3. Expo SQLite — dati relazionali offline
// catalogo prodotti, ordini, carrello, indirizzi
// 4. TanStack Query + MMKV — cache API
// feed prodotti, recensioni, raccomandazioni
// disponibili offline dalla cache persistente
// 5. WatermelonDB — (se necessario)
// solo per app con sync bidirezionale complesso
// e dataset molto grandi
FAQ
Qual è il miglior database locale per React Native nel 2026?
Non esiste una risposta unica — dipende dal tuo caso d'uso specifico. Per key-value storage, MMKV è la scelta migliore grazie alle prestazioni 20-30x superiori rispetto ad AsyncStorage. Per dati relazionali, Expo SQLite è l'opzione più pratica nell'ecosistema Expo, mentre op-sqlite offre prestazioni superiori per dataset molto grandi. Per app con sincronizzazione server e decine di migliaia di record, WatermelonDB rimane la soluzione più completa.
Come salvare dati sensibili come token di autenticazione in React Native?
Usa expo-secure-store per progetti Expo o react-native-keychain per progetti bare. Entrambi sfruttano il Keychain su iOS e l'EncryptedSharedPreferences su Android. Non salvare mai token in AsyncStorage o MMKV senza crittografia — i dati sarebbero accessibili in chiaro sul filesystem del dispositivo.
AsyncStorage è ancora valido nel 2026?
Per prototipi e dati molto semplici (poche preferenze), AsyncStorage funziona ancora. Ma per qualsiasi app in produzione, è fortemente consigliato migrare a MMKV per le prestazioni o al KV-Store di Expo SQLite come drop-in replacement con supporto per API sincrone. Il costo della migrazione è minimo e i benefici sono immediati.
Come funziona un'app React Native offline-first?
L'approccio più comune nel 2026 usa TanStack Query con MMKV persistence: i dati vengono scaricati dalle API, memorizzati nella cache locale e serviti immediatamente all'avvio, anche senza connessione. Per dati relazionali complessi con sync bidirezionale, WatermelonDB offre un protocollo pull/push integrato con gestione dei conflitti.
MMKV funziona con Expo Go?
No, purtroppo. MMKV v4 richiede la Nuova Architettura e moduli nativi compilati, quindi è necessario usare Expo Dev Client (tramite npx expo prebuild) oppure le development build di EAS. In Expo Go puoi usare AsyncStorage o il KV-Store di Expo SQLite come alternative temporanee.