tutoriales.com

Manejo Offline Avanzado en React Native con WatermelonDB y Sincronización

Descubre cómo construir aplicaciones móviles altamente responsivas y completamente funcionales sin conexión a internet utilizando WatermelonDB en React Native, optimizando el rendimiento y sincronizando con servidores remotos de forma eficiente.

Avanzado12 min de lectura4 views
Reportar error

Introducción al Desarrollo Offline-First en React Native 📱

En el ecosistema actual de desarrollo móvil, los usuarios esperan que sus aplicaciones funcionen perfectamente sin importar si tienen una conexión a internet estable o se encuentran completamente desconectados en el metro o en zonas remotas. La arquitectura Offline-First prioriza el almacenamiento local de los datos, permitiendo que la aplicación lea y escriba información instantáneamente en el dispositivo antes de intentar sincronizarla con un servidor remoto.

Cuando trabajamos con grandes volúmenes de datos relacionales en React Native, las soluciones tradicionales de almacenamiento local como AsyncStorage o incluso SQLite pueden volverse lentas y causar bloqueos en el hilo principal de JavaScript. Aquí es donde entra en juego WatermelonDB, una base de datos reactiva y extremadamente rápida construida sobre SQLite, diseñada específicamente para escalar con miles de registros sin comprometer la fluidez de la interfaz de usuario.

💡 Consejo: WatermelonDB utiliza perezosidad (lazy loading) y observables de RxJS para mantener tu interfaz ligera y actualizada en tiempo real ante cualquier cambio en la base de datos local.

¿Por qué Elegir WatermelonDB para tu App? 🍉

Antes de sumergirnos en el código, es fundamental comprender las ventajas competitivas que ofrece WatermelonDB frente a otras alternativas en el mercado móvil.

A continuación, presentamos una comparativa detallada de rendimiento y características:

CaracterísticaAsyncStorageSQLite TradicionalWatermelonDB
Rendimiento con 10,000 registrosMuy lentoModeradoUltra rápido
Reactividad nativaNoNoSí (con Observables)
Tipado estricto (TypeScript)LimitadoManualExcelente
Sincronización incorporadaNoNoSí (Synchronizer API)
Curva de aprendizajeBajaMediaIntermedia
DISPOSITIVO (OFFLINE) Interfaz de Usuario RxJS Observables WatermelonDB (Local) Sincronizador (Sync Engine) SERVIDOR (ONLINE) API Backend REST / GraphQL Evento Reactividad Cambios locales Sincronización

Requisitos Previos y Configuración del Entorno 🛠️

Para seguir este tutorial con éxito, asegúrate de tener instalado en tu entorno de desarrollo:

  • Node.js (versión 18 o superior)
  • React Native CLI (evitaremos Expo temporalmente debido a la necesidad de compilar código nativo para SQLite)
  • Un conocimiento básico de promesas, TypeScript y componentes funcionales de React

Instalación de Dependencias Principales

Primero, vamos a crear un nuevo proyecto o utilizar uno existente. En este tutorial asumiremos que usas React Native CLI.

Ejecuta los siguientes comandos en tu terminal para instalar WatermelonDB y sus dependencias de soporte para Babel y TypeScript:

npm install @nozbe/watermelondb
npm install --save-dev @babel/plugin-proposal-decorators

También necesitamos configurar Babel para soportar los decoradores de clases que utiliza WatermelonDB para definir modelos y esquemas de datos. Modifica tu archivo babel.config.js:

module.exports = {
  presets: ['module:@react-native/babel-preset'],
  plugins: [
    ['@babel/plugin-proposal-decorators', { legacy: true }],
  ],
};

Modelado de Datos y Esquemas 📐

El primer paso para estructurar nuestra base de datos local es definir el esquema global (schema.ts) y los modelos individuales de las tablas.

Creando el Esquema de la Base de Datos

Crea una carpeta llamada model en la raíz de tu proyecto y añade el archivo schema.ts:

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

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

Definiendo los Modelos

Ahora crearemos el modelo Task que interactuará directamente con la tabla tasks que acabamos de declarar. Crea el archivo model/Task.ts:

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

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

  @text('title') title!: string;
  @field('is_completed') isCompleted!: boolean;
  @readonly @date('created_at') createdAt!: Date;
  @readonly @date('updated_at') updatedAt!: Date;
}
📌 Nota: Los decoradores como @text y @field permiten mapear las propiedades de la clase TypeScript directamente a las columnas de la base de datos SQLite subyacente de forma segura y tipada.

Inicialización de la Base de Dios Local 🚀

Con el esquema y los modelos listos, debemos inicializar la instancia de la base de datos utilizando el adaptador nativo (SQLiteAdapter).

Crea un archivo model/index.ts:

import { Database } from '@nozbe/watermelondb';
import SQLiteAdapter from '@nozbe/watermelondb/adapters/sqlite';

import { mySchema } from './schema';
import Task from './Task';

const adapter = new SQLiteAdapter({
  schema: mySchema,
  // (Opcional) Migraciones futuras
  dbName: 'MyAppDatabase',
  jsi: true, // Habilita JSI para un rendimiento ultrarrápido en React Native
  onSetUpError: error => {
    console.error('Error al configurar la base de datos local:', error);
  },
});

export const database = new Database({
  adapter,
  modelClasses: [Task],
});

Operaciones CRUD Reactivas en la Interfaz ✍️

WatermelonDB destaca por su reactividad. En lugar de hacer consultas manuales cada vez que un usuario interactúa con la app, podemos observar colecciones enteras o registros individuales.

Creando y Actualizando Registros

Aquí tienes un ejemplo de cómo implementar funciones para añadir y modificar tareas localmente:

import { database } from '../model';
import Task from '../model/Task';

export async function createTask(title: string) {
  await database.write(async () => {
    await database.get<Task>('tasks').create(task => {
      task.title = title;
      task.isCompleted = false;
    });
  });
}

export async function toggleTaskCompletion(task: Task) {
  await database.write(async () => {
    await task.update(updatedTask => {
      updatedTask.isCompleted = !task.isCompleted;
    });
  });
}

Consumiendo Datos con Observables en Componentes

Para mostrar la lista de tareas de forma reactiva en un componente de React Native, utilizamos withObservables:

import React from 'react';
import { View, Text, FlatList, TouchableOpacity, StyleSheet } from 'react-native';
import { withObservables } from '@nozbe/watermelondb/react';
import { database } from '../model';
import Task from '../model/Task';
import { toggleTaskCompletion } from '../services/taskService';

const TaskList = ({ tasks }: { tasks: Task[] }) => {
  return (
    <FlatList
      data={tasks}
      keyExtractor={item => item.id}
      renderItem={({ item }) => (
        <TouchableOpacity
          style={styles.item}
          onPress={() => toggleTaskCompletion(item)}
        >
          <Text style={[styles.text, item.isCompleted && styles.completed]}>
            {item.title}
          </Text>
        </TouchableOpacity>
      )}
    />
  );
};

const enhance = withObservables([], () => ({
  tasks: database.get<Task>('tasks').query(),
}));

export default enhance(TaskList);

const styles = StyleSheet.create({
  item: { padding: 16, borderBottomWidth: 1, borderBottomColor: '#eee' },
  text: { fontSize: 16, color: '#333' },
  completed: { textDecorationLine: 'line-through', color: '#888' },
});

Sincronización Avanzada con Servidores Remotos 🌐

El verdadero poder de WatermelonDB reside en su API de sincronización nativa (synchronize). Esta función compara los cambios locales (changes) con los del servidor y resuelve conflictos automáticamente basándose en marcas de tiempo (updated_at).

Implementando la Lógica de Sincronización

A continuación, mostramos cómo estructurar la función de sincronización conectada a una API REST genérica:

import { synchronize } from '@nozbe/watermelondb/sync';
import { database } from '../model';

export async function syncDatabase() {
  await synchronize({
    database,
    pullChanges: async ({ lastPulledAt }) => {
      const response = await fetch(`https://api.miservidor.com/sync?last_pulled_at=${lastPulledAt || 0}`);
      if (!response.ok) {
        throw new Error('Error al obtener cambios del servidor');
      }
      const { changes, timestamp } = await response.json();
      return { changes, timestamp };
    },
    pushChanges: async ({ changes, lastPulledAt }) => {
      const response = await fetch('https://api.miservidor.com/sync', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ changes, last_pulled_at: lastPulledAt }),
      });
      if (!response.ok) {
        throw new Error('Error al enviar cambios al servidor');
      }
    },
    migrationsEnabledAtVersion: 1,
  });
}
⚠️ Advertencia: Asegúrate de envolver siempre tu proceso de sincronización en un bloque try/catch para manejar cortes de red repentinos y reintentar la sincronización cuando se restablezca la conexión.

Buenas Prácticas y Consejos de Rendimiento 🚀

Para garantizar que tu aplicación React Native con WatermelonDB se mantenga rápida y eficiente a lo largo del tiempo, ten en cuenta las siguientes recomendaciones:

Paso 1: Utiliza siempre transacciones de escritura masiva (database.write) cuando operes con múltiples registros para evitar escrituras repetitivas en disco.
Paso 2: Habilita JSI (JavaScript Interface) en la configuración del adaptador SQLite para reducir la sobrecarga de comunicación entre el puente nativo y JS.
Paso 3: Diseña índices adecuados en tu esquema para consultas frecuentes que involucren campos de filtrado masivo.

Resumen y Próximos Pasos

Has aprendido a configurar una arquitectura Offline-First avanzada en React Native utilizando WatermelonDB. Desde el modelado de datos con decoradores de TypeScript hasta la implementación de consultas reactivas y sincronización bidireccional con un servidor backend.

¿Preguntas Frecuentes (FAQ) sobre WatermelonDB

¿Puedo usar WatermelonDB con Expo? Sí, pero requiere el uso de Development Builds (Expo Dev Client) ya que WatermelonDB requiere código nativo compilado para SQLite y JSI.

¿Cómo manejo la autenticación de usuarios durante la sincronización? Puedes inyectar tokens de autorización Bearer en los encabezados (headers) de las peticiones fetch dentro de las funciones pullChanges y pushChanges.

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!