Storage Offline in React Native nel 2026: MMKV, Expo SQLite, WatermelonDB e TanStack Query a Confronto

Confronto pratico tra AsyncStorage, MMKV, Expo SQLite, op-sqlite e WatermelonDB per lo storage offline in React Native nel 2026. Benchmark reali, esempi di codice, integrazione con Zustand/Jotai e TanStack Query, e guida alla scelta per ogni caso d'uso.

Storage Offline React Native 2026: MMKV vs SQLite

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:

OperazioneAsyncStorageMMKVVelocità
Lettura2,548 ms0,520 ms~5x più veloce
Scrittura2,871 ms0,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 *Sync per 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

Caratteristicaop-sqliteExpo SQLite
Velocità query grandi (300k+ righe)Significativamente più veloceBuona
Query piccole (<100 righe)ComparabileComparabile
Plugin disponibiliSQLCipher, libSQL, FTS5, Rtree, cr-sqlite, sqlite-vecSQLCipher, libSQL, FTS, sqlite-vec
DevTools integratiNoSì (inspector browser)
ORM supportatiDrizzle, KyselyDrizzle, Kysely
Expo managed workflowConfig plugin necessarioIntegrato nativamente
Ricerca vettoriale (AI)sqlite-vec integratosqlite-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 datoSoluzione consigliata
Token JWT, refresh tokenexpo-secure-store
Chiavi API, segretiexpo-secure-store
Dati utente sensibiliMMKV + AES-256 (chiave dal Keychain)
Preferenze, impostazioniMMKV (senza crittografia)
Dati relazionaliExpo SQLite + SQLCipher
Cache APITanStack 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.

Sull'Autore Editorial Team

Our team of expert writers and editors.