Bases de Datos Locales en React Native 2026: expo-sqlite, MMKV, WatermelonDB y Offline-First

Aprende a implementar bases de datos locales en React Native con expo-sqlite, Drizzle ORM, MMKV, WatermelonDB y op-sqlite. Ejemplos prácticos, benchmarks y arquitectura offline-first paso a paso.

¿Por qué necesitas una base de datos local en React Native en 2026?

Seamos honestos: en 2026 los usuarios no tienen paciencia. Esperan que tu app responda al instante, que funcione cuando se meten al metro sin señal y que todo se sincronice mágicamente cuando vuelvan a conectarse. La arquitectura offline-first dejó de ser un "nice to have" hace rato — ahora es un requisito real para cualquier app seria construida con React Native.

Y aquí viene lo bueno. Con la Nueva Arquitectura de React Native habilitada por defecto (React Native 0.76+ y Expo SDK 52+), las librerías de bases de datos locales usan JSI (JavaScript Interface) para hablar directamente con el código nativo de forma síncrona. Se acabó el viejo Bridge asíncrono. El resultado es un rendimiento hasta 30 veces superior al de AsyncStorage.

En esta guía vamos a revisar las principales opciones de almacenamiento local disponibles hoy, con ejemplos de código que puedes copiar y probar, benchmarks reales y una guía paso a paso para implementar offline-first en tu app.

Arquitectura Offline-First: el patrón que tu app necesita

El enfoque offline-first invierte el flujo tradicional de datos. En vez de consultar primero al servidor, la app trabaja contra la base de datos local como fuente principal de verdad:

  1. Lectura: los datos se leen directamente desde la base de datos local — sin latencia de red.
  2. Escritura: los cambios se guardan primero en local, así que la app funciona sin conexión.
  3. Sincronización: los cambios se envían al servidor cuando hay conectividad.
  4. Resolución de conflictos: se fusionan cambios concurrentes de forma determinista.

El resultado es una app que arranca rápido, responde al instante y nunca muestra un spinner de carga innecesario. Personalmente, desde que adopté este patrón no he vuelto al enfoque tradicional — la diferencia en experiencia de usuario es enorme.

expo-sqlite + Drizzle ORM: la combinación moderna

expo-sqlite es la solución oficial de Expo para SQLite en React Native. Cuando lo combinas con Drizzle ORM, obtienes un stack completamente tipado, con migraciones automáticas y live queries que re-renderizan la UI sola cuando cambian los datos. Es una combinación bastante elegante, la verdad.

Instalación y configuración

Primero, instala las dependencias:

npx expo install expo-sqlite
npm install drizzle-orm
npm install -D drizzle-kit

Ahora configura drizzle.config.ts en la raíz del proyecto:

import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  dialect: 'sqlite',
  driver: 'expo',
  schema: './db/schema.ts',
  out: './drizzle',
});

Definir el esquema

Crea tu esquema en db/schema.ts usando la sintaxis de Drizzle. Fíjate en cómo puedes inferir los tipos automáticamente al final — esto te ahorra mucho boilerplate:

import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';

export const tasks = sqliteTable('tasks', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  title: text('title').notNull(),
  description: text('description'),
  completed: integer('completed', { mode: 'boolean' }).default(false),
  createdAt: text('created_at').notNull(),
});

export type Task = typeof tasks.$inferSelect;
export type NewTask = typeof tasks.$inferInsert;

Generar migraciones

Añade un script en package.json y genera las migraciones. Es un solo comando:

// package.json
{
  "scripts": {
    "db:generate": "drizzle-kit generate"
  }
}

// Ejecutar
npm run db:generate

Inicializar la base de datos con migraciones

En tu layout principal, usa SQLiteProvider con Suspense para asegurarte de que la base de datos esté lista antes de renderizar. Esto es clave para evitar errores en el primer render:

import { Suspense } from 'react';
import { SQLiteProvider } from 'expo-sqlite';
import { drizzle } from 'drizzle-orm/expo-sqlite';
import { useMigrations } from 'drizzle-orm/expo-sqlite/migrator';
import { openDatabaseSync } from 'expo-sqlite';
import migrations from './drizzle/migrations';

const expoDb = openDatabaseSync('myapp.db', {
  enableChangeListener: true,
});
export const db = drizzle(expoDb);

function MigrationProvider({ children }: { children: React.ReactNode }) {
  const { success, error } = useMigrations(db, migrations);

  if (error) return <Text>Error en migración: {error.message}</Text>;
  if (!success) return <Text>Ejecutando migraciones...</Text>;

  return <>{children}</>;
}

export default function RootLayout() {
  return (
    <Suspense fallback={<Text>Cargando base de datos...</Text>}>
      <MigrationProvider>
        <App />
      </MigrationProvider>
    </Suspense>
  );
}

Live Queries: UI reactiva automática

Esto es lo que más me gusta de esta combinación. Desde Drizzle ORM v0.31.1, el hook useLiveQuery observa cambios en la base de datos y re-ejecuta las consultas automáticamente. Nada de suscripciones manuales ni callbacks complicados:

import { useLiveQuery } from 'drizzle-orm/expo-sqlite';
import { db } from './database';
import { tasks } from './db/schema';
import { eq } from 'drizzle-orm';

function TaskList() {
  const { data: pendingTasks } = useLiveQuery(
    db.select().from(tasks).where(eq(tasks.completed, false))
  );

  return (
    <FlatList
      data={pendingTasks}
      keyExtractor={(item) => item.id.toString()}
      renderItem={({ item }) => (
        <Text>{item.title}</Text>
      )}
    />
  );
}

Depuración con Drizzle Studio

Un detalle que vale la pena mencionar: puedes depurar tu base de datos visualmente con el plugin de Drizzle Studio para Expo. Solo presiona shift + m en la terminal del servidor de desarrollo, selecciona expo-drizzle-studio-plugin y se abre Drizzle Studio en tu navegador conectado directamente a tu SQLite. Bastante útil para verificar que tus datos están como esperas.

react-native-mmkv: almacenamiento clave-valor ultrarrápido

MMKV es una librería de almacenamiento clave-valor creada por Tencent (sí, los de WeChat) y portada a React Native por Marc Rousavy. Para que te hagas una idea: es hasta 30-100 veces más rápida que AsyncStorage. Es la opción ideal para datos ligeros como preferencias de usuario, tokens de sesión y configuraciones.

MMKV v4: Nitro Module

La versión 4 de react-native-mmkv es ahora un Nitro Module, lo que requiere React Native 0.74+ con la Nueva Arquitectura habilitada:

npx expo install react-native-mmkv react-native-nitro-modules
npx expo prebuild

Uso básico

La API es directa y síncrona. Sin promesas, sin await, sin callbacks:

import { MMKV } from 'react-native-mmkv';

const storage = new MMKV();

// Guardar datos
storage.set('user.name', 'María');
storage.set('user.age', 28);
storage.set('onboarding.completed', true);

// Leer datos
const name = storage.getString('user.name'); // 'María'
const age = storage.getNumber('user.age');     // 28
const done = storage.getBoolean('onboarding.completed'); // true

// Eliminar
storage.delete('user.name');

// Verificar existencia
const exists = storage.contains('user.age'); // true

MMKV con cifrado

¿Necesitas guardar datos sensibles como tokens de autenticación? Puedes crear instancias cifradas sin mucho esfuerzo:

const secureStorage = new MMKV({
  id: 'secure-vault',
  encryptionKey: 'mi-clave-secreta-2026',
});

secureStorage.set('auth.token', 'eyJhbGciOiJIUzI1NiIs...');
secureStorage.set('auth.refreshToken', 'dGhpcyBpcyBhIHJl...');

Persistir estado de Zustand con MMKV

Bueno, esta es una de mis combinaciones favoritas en 2026. Usar Zustand + MMKV para persistir el estado de la app es ridículamente rápido. El paquete zustand-mmkv-storage simplifica bastante la integración:

npm install zustand zustand-mmkv-storage react-native-mmkv
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { mmkvStorage } from 'zustand-mmkv-storage';

interface UserStore {
  theme: 'light' | 'dark';
  language: string;
  setTheme: (theme: 'light' | 'dark') => void;
  setLanguage: (lang: string) => void;
}

export const useUserStore = create<UserStore>()(
  persist(
    (set) => ({
      theme: 'light',
      language: 'es',
      setTheme: (theme) => set({ theme }),
      setLanguage: (language) => set({ language }),
    }),
    {
      name: 'user-preferences',
      storage: createJSONStorage(() => mmkvStorage),
    }
  )
);

Con esta configuración, el estado se persiste de forma síncrona en MMKV. Al reiniciar la app, las preferencias se restauran instantáneamente — nada de ese molesto flash del estado inicial que ves con AsyncStorage.

WatermelonDB: rendimiento con grandes datasets

WatermelonDB (v0.28) es una base de datos reactiva construida sobre SQLite, diseñada específicamente para React Native. Su punto fuerte es el rendimiento con grandes conjuntos de datos: incluso con más de 10,000 registros, la mayoría de consultas se resuelven en menos de 1 milisegundo. Sí, leíste bien.

Características clave

  • Lazy loading: solo carga datos cuando realmente se necesitan, minimizando el uso de memoria.
  • Consultas observables: la UI se actualiza sola cuando cambian los datos (basado en RxJS).
  • Hilo nativo: las consultas SQL corren en un hilo separado, sin bloquear la interfaz.
  • Soporte JSI: compatible con la Nueva Arquitectura de React Native.
  • Tamaño reducido: añade solo ~2 MB al tamaño de la app (bastante razonable).

Instalación

npm install @nozbe/watermelondb
npm install -D @babel/plugin-proposal-decorators

Definir modelos

WatermelonDB usa decoradores para definir los modelos. Si vienes de un background con ORMs como TypeORM, te resultará familiar:

import { Model } from '@nozbe/watermelondb';
import { field, text, date, readonly } from '@nozbe/watermelondb/decorators';

class Task extends Model {
  static table = 'tasks';

  @text('title') title;
  @text('description') description;
  @field('is_completed') isCompleted;
  @readonly @date('created_at') createdAt;
  @readonly @date('updated_at') updatedAt;
}

Definir el esquema

import { appSchema, tableSchema } from '@nozbe/watermelondb';

export const schema = appSchema({
  version: 1,
  tables: [
    tableSchema({
      name: 'tasks',
      columns: [
        { name: 'title', type: 'string' },
        { name: 'description', type: 'string', isOptional: true },
        { name: 'is_completed', type: 'boolean' },
        { name: 'created_at', type: 'number' },
        { name: 'updated_at', type: 'number' },
      ],
    }),
  ],
});

Consultas reactivas con el nuevo API de React

Desde la v0.27, todos los helpers de React están disponibles desde @nozbe/watermelondb/react. Las consultas observables se suscriben a cambios y actualizan la UI automáticamente:

import { useDatabase } from '@nozbe/watermelondb/react';
import { Q } from '@nozbe/watermelondb';

function TaskList() {
  const database = useDatabase();

  const [tasks, setTasks] = useState([]);

  useEffect(() => {
    const subscription = database
      .get('tasks')
      .query(Q.where('is_completed', false))
      .observe()
      .subscribe((result) => setTasks(result));

    return () => subscription.unsubscribe();
  }, [database]);

  return (
    <FlatList
      data={tasks}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => <Text>{item.title}</Text>}
    />
  );
}

op-sqlite: rendimiento máximo con JSI

Si necesitas exprimir hasta el último milisegundo de rendimiento con SQLite, op-sqlite es tu librería. Creada por Oscar Franco, ofrece acceso sincrónico directo a SQLite mediante JSI, con mejoras de hasta 5x en velocidad y 5x menos memoria comparado con librerías anteriores. Es particularmente buena para casos como analíticas on-device o cargas de trabajo pesadas.

npm install @op-engineering/op-sqlite
npx expo prebuild
import { open } from '@op-engineering/op-sqlite';

const db = open({ name: 'myapp.sqlite' });

// Crear tabla
db.execute(
  'CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, email TEXT)'
);

// Insertar con transacción
db.transaction(() => {
  db.execute('INSERT INTO users (name, email) VALUES (?, ?)', [
    'Carlos',
    '[email protected]',
  ]);
  db.execute('INSERT INTO users (name, email) VALUES (?, ?)', [
    'Ana',
    '[email protected]',
  ]);
});

// Consultar
const { rows } = db.execute('SELECT * FROM users WHERE name LIKE ?', ['%Car%']);
console.log(rows); // [{ id: 1, name: 'Carlos', email: '[email protected]' }]

Consejo: activa el modo WAL (Write-Ahead Logging) para mejorar significativamente el rendimiento cuando tienes muchas escrituras pequeñas. Es un cambio de una línea que puede marcar una diferencia notable:

db.execute('PRAGMA journal_mode = WAL');
db.execute('PRAGMA synchronous = NORMAL');

Sincronización: de local a la nube

Tener una base de datos local está muy bien, pero para la mayoría de apps de producción necesitas sincronizar esos datos con un servidor. Veamos las principales opciones disponibles en 2026.

PowerSync

PowerSync (v1.29.0) es un motor de sincronización que conecta SQLite en el cliente con Postgres, MongoDB o MySQL en el servidor. Su SDK de React Native soporta Expo y ofrece sincronización en tiempo real, soporte offline completo y prioridades de sincronización por bucket.

npm install @powersync/react-native

PowerSync es ideal cuando ya tienes un backend con Postgres o MongoDB y quieres sincronización bidireccional sin tener que construir toda la lógica de conflictos tú mismo.

RxDB

RxDB es una base de datos reactiva offline-first con adaptadores de sincronización para CouchDB, Supabase, Firestore y más. Tiene soporte completo de TypeScript, una arquitectura de plugins y te permite construir tu app como si fuera puramente local mientras la replicación ocurre en segundo plano. Si tu backend no es Postgres, vale la pena echarle un vistazo.

Sincronización manual con WatermelonDB

WatermelonDB no incluye sincronización integrada (es una decisión de diseño deliberada), pero proporciona primitivas de sincronización. Básicamente, necesitas crear dos endpoints en tu backend: uno para push (enviar cambios locales al servidor) y otro para pull (descargar cambios del servidor). Requiere más trabajo manual, pero te da control total sobre la lógica.

Comparativa: ¿cuál elegir?

Bien, vamos al grano. Esta tabla resume las principales opciones según lo que necesites:

Necesidad Solución recomendada Por qué
Preferencias y tokens MMKV 30-100x más rápido que AsyncStorage, síncrono, cifrado opcional
Datos relacionales con Expo expo-sqlite + Drizzle Oficial de Expo, tipado, live queries, soporte web
Máximo rendimiento SQLite op-sqlite 5x más rápido y 5x menos memoria que alternativas anteriores
Grandes datasets (+10k registros) WatermelonDB Lazy loading, consultas <1ms, probado en producción
Sync con Postgres/MongoDB PowerSync Sincronización bidireccional transparente, offline completo
Sync con múltiples backends RxDB Adaptadores para CouchDB, Supabase, Firestore y más

Guía de decisión paso a paso

Si aún no tienes claro cuál elegir, sigue este flujo:

  1. ¿Solo necesitas guardar configuraciones o tokens? → Usa MMKV. No necesitas una base de datos relacional para datos simples de clave-valor.
  2. ¿Usas Expo y necesitas datos relacionales? → Empieza con expo-sqlite + Drizzle ORM. Es la opción más integrada con el ecosistema Expo y probablemente la que menos fricción te dará.
  3. ¿Tu app maneja más de 10,000 registros y necesitas UI reactiva? → Considera WatermelonDB. Su lazy loading y consultas observables están optimizados para exactamente ese escenario.
  4. ¿Necesitas rendimiento extremo con consultas complejas? → Usa op-sqlite. Ideal para analíticas on-device o cargas de trabajo de IA local.
  5. ¿Necesitas sincronización con un backend? → Evalúa PowerSync para Postgres/MongoDB, o RxDB si necesitas flexibilidad con múltiples backends.

Bonus: reemplazar AsyncStorage con expo-sqlite/kv-store

Un truco rápido que no mucha gente conoce. Si tu proyecto ya usa expo-sqlite, puedes reemplazar AsyncStorage con el almacén clave-valor integrado, sin añadir ninguna dependencia extra:

import { Storage } from 'expo-sqlite/kv-store';

// API compatible con AsyncStorage
await Storage.setItem('user.token', 'abc123');
const token = await Storage.getItem('user.token');
await Storage.removeItem('user.token');

Es una alternativa directa a @react-native-async-storage/async-storage, respaldada por SQLite. Útil si quieres reducir dependencias en tu proyecto sin cambiar demasiado código.

Preguntas frecuentes

¿Cuál es la base de datos local más rápida para React Native en 2026?

Depende del tipo de datos. Para almacenamiento clave-valor, MMKV es la más rápida (30-100x más que AsyncStorage). Para bases de datos relacionales, op-sqlite ofrece el máximo rendimiento gracias a JSI síncrono, siendo hasta 5x más rápida que las alternativas anteriores. Y si necesitas una API reactiva con buen rendimiento en datasets grandes, WatermelonDB resuelve la mayoría de consultas en menos de 1 milisegundo.

¿Puedo usar SQLite con Expo sin hacer eject?

Sí, totalmente. expo-sqlite es una librería oficial de Expo que funciona tanto en el flujo gestionado como en builds de desarrollo. Solo necesitas ejecutar npx expo prebuild para generar los proyectos nativos. Con Drizzle ORM integrado, obtienes migraciones automáticas, tipado completo y live queries sin eject.

¿Cómo persisto el estado de Zustand en React Native de forma rápida?

La forma más eficiente es combinar el middleware persist de Zustand con MMKV como backend de almacenamiento. El paquete zustand-mmkv-storage proporciona un adaptador listo para usar con soporte para cifrado, carga diferida y caché de instancias. Al ser síncrono, elimina ese flash de estado inicial tan molesto al reiniciar la app.

¿WatermelonDB soporta la Nueva Arquitectura de React Native?

Sí. WatermelonDB usa JSI para operaciones síncronas con SQLite nativo, lo que lo hace compatible con la Nueva Arquitectura (Fabric + TurboModules). El equipo reescribió las implementaciones nativas a Objective-C y Java para garantizar compatibilidad total, eliminando los problemas previos que tenían con Kotlin y Swift.

¿Cuándo debería usar PowerSync en lugar de implementar sincronización manual?

Usa PowerSync cuando tu backend sea Postgres, MongoDB o MySQL y necesites sincronización bidireccional confiable sin construir tu propia lógica de conflictos. PowerSync v1.29 incluye un cliente Rust por defecto, progreso de descarga en tiempo real y prioridades de sincronización por bucket. Honestamente, implementar sincronización manual solo tiene sentido si tienes requisitos muy específicos de resolución de conflictos o usas un backend no estándar.

Sobre el Autor Editorial Team

Our team of expert writers and editors.