Concepto clave
Un sistema de caché offline es como tener una despensa en tu cocina. Cuando tienes conexión a internet, llenas esa despensa con productos (datos) que sabes que vas a necesitar. Cuando la conexión falla, en lugar de ir al supermercado (servidor), puedes usar lo que ya tienes almacenado. En aplicaciones móviles, esto significa almacenar datos localmente en el dispositivo usando Expo SQLite para que la app siga funcionando sin conexión.
La arquitectura típica implica una capa de sincronización bidireccional: cuando hay conexión, los datos se sincronizan entre el servidor y la base de datos local; cuando no hay conexión, la app opera solo con los datos locales. Esto mejora la experiencia del usuario, reduce la dependencia de la red y puede optimizar el rendimiento al minimizar llamadas a la API.
Cómo funciona en la práctica
Imagina que estás construyendo una app de lista de tareas. Sin caché offline, cada vez que abres la app necesitas cargar las tareas desde el servidor. Con nuestro sistema, el flujo sería:
- Al iniciar la app, verificar si hay conexión a internet.
- Si hay conexión, sincronizar: descargar tareas nuevas del servidor y subir cambios locales.
- Si no hay conexión, cargar todas las tareas desde SQLite.
- Cuando el usuario crea, actualiza o elimina una tarea, guardar primero en SQLite y marcar para sincronización posterior.
- En segundo plano, cuando se detecte conexión, procesar la cola de sincronización.
Este enfoque garantiza que el usuario siempre vea datos actualizados (aunque puedan estar ligeramente desfasados sin conexión) y que sus cambios nunca se pierdan.
Código en acción
Primero, configuramos la base de datos SQLite:
import * as SQLite from 'expo-sqlite';
const db = SQLite.openDatabase('offline_cache.db');
// Inicializar tablas
export const initDatabase = () => {
return new Promise((resolve, reject) => {
db.transaction(tx => {
tx.executeSql(
`CREATE TABLE IF NOT EXISTS tasks (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
completed INTEGER DEFAULT 0,
created_at TEXT,
updated_at TEXT,
sync_status TEXT DEFAULT 'pending'
)`,
[],
() => resolve(),
(_, error) => reject(error)
);
});
});
};
Ahora, implementamos la lógica de sincronización:
// Antes: Llamada directa a la API
const fetchTasks = async () => {
try {
const response = await fetch('https://api.example.com/tasks');
return await response.json();
} catch (error) {
throw new Error('Network error');
}
};
// Después: Con caché offline
export const getTasks = async () => {
const isOnline = await checkConnectivity();
if (isOnline) {
try {
const tasks = await fetchTasks();
await saveTasksToLocal(tasks);
return tasks;
} catch (error) {
// Fallback a datos locales si la API falla
return getLocalTasks();
}
} else {
return getLocalTasks();
}
};
const saveTasksToLocal = (tasks) => {
return new Promise((resolve, reject) => {
db.transaction(tx => {
tasks.forEach(task => {
tx.executeSql(
'INSERT OR REPLACE INTO tasks (id, title, completed, created_at, updated_at, sync_status) VALUES (?, ?, ?, ?, ?, ?)',
[task.id, task.title, task.completed ? 1 : 0, task.created_at, task.updated_at, 'synced'],
() => {},
(_, error) => reject(error)
);
});
}, reject, resolve);
});
};
Errores comunes
- No manejar conflictos de sincronización: Cuando el mismo dato se modifica tanto localmente como en el servidor, necesitas una estrategia de resolución de conflictos (como "última modificación gana" o fusión manual).
- Olvidar limpiar datos obsoletos: SQLite puede llenarse con datos antiguos. Implementa un mecanismo de expiración o limpieza periódica.
- Asumir que SQLite es asíncrono por defecto: Las operaciones de SQLite en Expo son síncronas en el hilo principal. Usa transacciones y promesas para no bloquear la UI.
- No probar escenarios de conexión intermitente: Simula pérdida de conexión durante la sincronización para asegurar que tu app se recupera correctamente.
- Ignorar el tamaño de la base de datos: En dispositivos con almacenamiento limitado, monitorea el tamaño de tu base de datos y comprime datos cuando sea posible.
Checklist de dominio
- ¿Puedes inicializar una base de datos SQLite en Expo y crear tablas con esquemas apropiados?
- ¿Implementas correctamente la detección de conexión a internet usando NetInfo de React Native?
- ¿Manejas tanto el flujo online (sincronización bidireccional) como offline (operaciones locales)?
- ¿Tienes una estrategia para resolver conflictos cuando los datos se modifican tanto localmente como en el servidor?
- ¿Incluyes manejo de errores robusto para fallos de red, errores de base de datos y excepciones inesperadas?
- ¿Optimizas las consultas SQL con índices en campos frecuentemente buscados?
- ¿Proteges datos sensibles en SQLite usando encriptación cuando es necesario?
Implementa un sistema de caché offline para una app de notas
En este ejercicio, crearás un sistema de caché offline completo para una aplicación de notas usando Expo SQLite. Sigue estos pasos:
- Configuración inicial: Crea un nuevo proyecto Expo e instala las dependencias necesarias:
expo-sqlitey@react-native-community/netinfo. - Base de datos: Implementa la función
initDatabase()que cree una tablanotescon los campos: id (TEXT PRIMARY KEY), title (TEXT), content (TEXT), createdAt (TEXT), updatedAt (TEXT), syncStatus (TEXT). - Operaciones CRUD locales: Crea funciones para crear, leer, actualizar y eliminar notas en SQLite. Marca las notas nuevas o modificadas con
syncStatus = 'pending'. - Detección de conexión: Usa NetInfo para detectar cuando la app está online u offline. Muestra un indicador visual en la UI.
- Sincronización: Implementa una función
syncNotes()que, cuando hay conexión:- Descargue notas del servidor (simula con un array mock) y las guarde localmente.
- Suba las notas con
syncStatus = 'pending'al servidor (simula con console.log) y actualice su estado a'synced'.
- Integración: Modifica tu componente principal para que:
- Al cargar, primero intente sincronizar si hay conexión, luego muestre notas locales.
- Al crear/editar una nota, guarde primero localmente y luego intente sincronizar en segundo plano.
- Pruebas: Simula escenarios offline creando notas sin conexión, luego conecta y verifica que se sincronicen automáticamente.
- Usa async/await para manejar las operaciones asíncronas de SQLite y red de forma legible.
- Considera usar un contexto de React o un estado global (como Redux) para manejar el estado de conexión en toda la app.
- Para simular el servidor, crea un módulo mock que devuelva promesas con datos de ejemplo y simule latencia de red.
Evalua tu comprension
Completa el quiz interactivo de arriba para ganar XP.