Persistencia Local y Almacenamiento Seguro
Volver a clases
Desarrollo Móvil●●Intermedio

Persistencia Local y Almacenamiento Seguro

120 min

Tu app se cierra y el usuario pierde todo. Dónde guardar preferencias, dónde guardar un token, y por qué no es el mismo lugar.

Etiquetas

#asyncstorage#securestore#persistencia

El problema: tu app se cierra todo el tiempo

En web, el usuario tiene la pestaña abierta y el estado vive mientras no recargue. En móvil pasa lo contrario: el usuario cambia de app cada dos minutos, el sistema operativo mata tu proceso cuando necesita memoria, y al volver el usuario espera encontrar todo como lo dejó.

Sin persistencia, cada apertura es un borrón y cuenta nueva: sesión perdida, preferencias olvidadas, pantalla vacía mientras carga. Persistir no es una funcionalidad extra en móvil — es lo mínimo para que la app se sienta seria.

Las tres capas

Casi todo lo que vas a guardar cae en una de estas tres categorías, y cada una tiene su lugar:

  1. Preferencias — tema, idioma, filtros, orden. Pequeñas, no sensibles, se leen al arrancar. → AsyncStorage.
  2. Caché — la última respuesta de tu API, para mostrar algo mientras llega lo nuevo (y para funcionar sin red, que es la clase siguiente). → AsyncStorage.
  3. Credenciales — token de sesión, PIN, llaves. Comprometerlas compromete la cuenta. → SecureStore.

La tercera tabla de snippets resuelve la pregunta caso por caso, incluida la categoría más importante: lo que no se guarda nunca.

AsyncStorage: dos reglas y un error clásico

AsyncStorage es un almacén clave-valor, y todo en él tiene dos características que hay que interiorizar:

  • Es asíncrono. Toda operación devuelve una promesa. Olvidar el await es el error número uno: comparas contra una promesa y la condición nunca se cumple.
  • Solo guarda strings. Objetos y arrays pasan por JSON.stringify al guardar y JSON.parse al leer. Y al leer siempre puede venir null (la primera vez no hay nada), así que siempre defines un valor por defecto.

Lo que casi nadie te dice de AsyncStorage

🚨 AsyncStorage no cifra nada. Es texto plano dentro del sandbox de la app. En un dispositivo con root, con acceso físico o desde un respaldo del sistema, se lee sin esfuerzo.

Eso no lo hace malo: lo hace adecuado para cierto tipo de datos. Que tu tema visual esté en texto plano no le importa a nadie. Que el token de sesión lo esté, sí.

SecureStore: el almacén del sistema operativo

Para credenciales existe expo-secure-store, que no inventa su propio cifrado sino que delega en lo que cada plataforma ya tiene:

  • iOS → Keychain
  • Android → Keystore

Ambos están respaldados por hardware en dispositivos modernos, lo que significa que la llave de cifrado no es extraíble ni siquiera con acceso físico. Además puedes exigir biometría para leer un valor (requireAuthentication), y entonces ni con el teléfono desbloqueado se obtiene el dato sin la huella del dueño.

Su límite: está pensado para valores pequeños. Guarda credenciales, no catálogos.

El cierre con la clase de seguridad

En el curso web discutimos dónde guardar un token: localStorage (que cualquier XSS lee) contra cookie httpOnly (que JavaScript no puede tocar). En móvil ninguna de las dos existe, y la respuesta correcta es distinta.

Pero fíjate en lo que no cambia: la pregunta. ¿Qué estoy aceptando al guardar esto aquí? ¿Quién más podría leerlo? Eso es lo que se espera de ti cada vez que cambies de plataforma o de tecnología — las APIs se aprenden en una tarde; el criterio es lo que traes contigo.

Cerrar sesión de verdad

Un detalle que casi todos fallan: cerrar sesión no es borrar el token. Es borrar todo lo asociado a ese usuario — perfil en caché, borradores, listas guardadas. Si el teléfono se comparte o se vende, lo que quedó ahí sigue siendo del usuario anterior.

Y lo inverso también importa: un token guardado no significa sesión válida. Verificar la expiración antes de dar por buena la sesión evita la peor experiencia posible — una app que se ve funcional y falla en cada petición.

En tu proyecto

Tu proyecto integrador ahora exige persistir al menos un dato entre sesiones. Lo mínimo es una preferencia; lo ideal es que tu lista principal se vea al instante desde la caché mientras se actualiza en segundo plano. Ese pequeño truco es la diferencia entre una app que se siente rápida y una que se siente lenta — y es exactamente lo que necesitas para la clase siguiente, cuando la red desaparezca.

Ejemplos de Código

4 ejemplos

AsyncStorage — todo es asíncrono y todo es string

Las dos reglas que causan el 90% de los errores iniciales.

javascript
1import AsyncStorage from "@react-native-async-storage/async-storage";
2
3// Guardar: SIEMPRE string → serializa lo que no lo sea
4await AsyncStorage.setItem("tema", "oscuro");
5await AsyncStorage.setItem("filtros", JSON.stringify({ orden: "precio" }));
6
7// Leer: devuelve string o null (nunca undefined)
8const tema = await AsyncStorage.getItem("tema");           // "oscuro" | null
9const crudo = await AsyncStorage.getItem("filtros");
10const filtros = crudo ? JSON.parse(crudo) : { orden: "fecha" };
11//              ↑ SIEMPRE un valor por defecto: la primera vez no hay nada
12
13await AsyncStorage.removeItem("tema");
14
15// 🚨 Error clásico: olvidar el await
16const t = AsyncStorage.getItem("tema");   // ← esto es una Promesa
17if (t === "oscuro") { }                   // ← nunca entra

SecureStore — para lo que no puede filtrarse

Keychain en iOS, Keystore en Android. Cifrado por el sistema operativo.

javascript
1import * as SecureStore from "expo-secure-store";
2
3await SecureStore.setItemAsync("token", jwt);
4const token = await SecureStore.getItemAsync("token");   // string | null
5await SecureStore.deleteItemAsync("token");
6
7// Con protección biométrica (huella / Face ID) al leer:
8await SecureStore.setItemAsync("token", jwt, {
9  requireAuthentication: true,
10});
11
12Límites que conviene conocer:
13Pensado para valores pequeños (~2 KB): credenciales, no catálogos.
14 No está disponible en web; si tu proyecto es universal, necesitas
15  una rama por plataforma.

Qué va dónde

La tabla que resuelve la pregunta de cada dato.

text
1DATO                          DÓNDE            POR QUÉ
2──────────────────────────────────────────────────────────────────
3Tema, idioma, filtros         AsyncStorage     no es sensible
4Caché de listados             AsyncStorage     acelera el arranque
5Borrador de un formulario     AsyncStorage     evita perder trabajo
6──────────────────────────────────────────────────────────────────
7Token de sesión               SecureStore      da acceso a la cuenta
8Claves de API del usuario     SecureStore      credencial
9PIN de la app                 SecureStore      credencial
10──────────────────────────────────────────────────────────────────
11Contraseña                    NO SE GUARDA     para eso está el token
12Tarjeta de crédito            NO SE GUARDA     usa token de la pasarela
13Datos de otros usuarios       NO SE GUARDA     no son tuyos
14──────────────────────────────────────────────────────────────────
15Estado de la pantalla actual  memoria          se recalcula al abrir

Un hook para no repetir el patrón

Cargar al montar, guardar al cambiar, con estado de carga honesto.

javascript
1function usePersistedState(clave, valorInicial) {
2  const [valor, setValor] = useState(valorInicial);
3  const [cargando, setCargando] = useState(true);
4
5  // Cargar una vez al montar
6  useEffect(() => {
7    let cancelado = false;
8    AsyncStorage.getItem(clave)
9      .then((crudo) => {
10        if (cancelado) return;
11        if (crudo != null) setValor(JSON.parse(crudo));
12      })
13      .finally(() => !cancelado && setCargando(false));
14
15    return () => { cancelado = true; };   // ← el cleanup de siempre
16  }, [clave]);
17
18  // Guardar cuando cambie (ya cargado)
19  useEffect(() => {
20    if (!cargando) AsyncStorage.setItem(clave, JSON.stringify(valor));
21  }, [clave, valor, cargando]);
22
23  return [valor, setValor, cargando];
24}
25
26// 🚨 Sin la bandera `cargando`, el segundo efecto corre en el primer
27// render y SOBREESCRIBE lo guardado con el valor inicial.

Recursos

4 recursos disponibles

¡Hora de Practicar!

PrácticaIntermedio45 min🟡 IA con bitácora

Práctica Guiada — Que tu app recuerde

Persistir preferencias, caché y sesión en el proyecto integrador.

Sobre tu proyecto, implementa las tres capas de persistencia:

  1. Preferencia — guarda algo que el usuario elige (tema oscuro, orden de la lista, filtro favorito) con AsyncStorage. Ciérra la app por completo y vuelve a abrirla: debe recordarlo.
  2. Caché — guarda la última respuesta de tu API. Al abrir la app, muestra primero lo guardado y actualiza cuando llegue lo nuevo. El usuario nunca debe ver una pantalla vacía si ya tuvo datos.
  3. Credencial — si tu proyecto tiene sesión, guarda el token con expo-secure-store. Si no tiene sesión, guarda cualquier dato que consideres sensible y justifica en un comentario por qué va ahí y no en AsyncStorage.
  4. Cierre de sesión — implementa el borrado. Verifica que después de cerrar sesión no quede nada recuperable.

Entrega: commits del proyecto + una nota en tu SEGURIDAD.md con la tabla de qué guardaste y dónde.

Desafío de Código

EjercicioIntermedio25 min🔴 Sin IA

Ejercicios — ¿Dónde va cada dato?

Clasificar antes de guardar es la mitad del trabajo.

Para cada dato di dónde lo guardarías (AsyncStorage, SecureStore, solo en memoria o no se guarda) y por qué:

  1. El tema visual elegido por el usuario (claro/oscuro)
  2. El token de sesión (JWT)
  3. La contraseña del usuario
  4. El último resultado de búsqueda, para mostrarlo al reabrir
  5. El número de tarjeta con el que pagó
  6. Un borrador del formulario que estaba llenando cuando entró una llamada
  7. El id del usuario
  8. La huella/biometría

Después responde: si el teléfono se pierde y alguien saca un respaldo del dispositivo, ¿qué de tu lista queda expuesto?

Reto de Lectura

Reto de LecturaIntermedio35 min🔴 Sin IA

Reto de Lectura — El token en la gaveta equivocada

Este manejo de sesión funciona perfecto y deja al usuario expuesto. Cinco decisiones equivocadas sobre qué se guarda, dónde y hasta cuándo.

Este código maneja la sesión de una app de finanzas personales. Funciona: el usuario entra, cierra la app, la vuelve a abrir y sigue dentro. Encuentra las 5 fallas y ordénalas por gravedad.

jsx
1import AsyncStorage from "@react-native-async-storage/async-storage";
2
3export async function login(correo, password) {
4  const r = await fetch("https://api.miapp.com/login", {
5    method: "POST",
6    body: JSON.stringify({ correo, password }),
7  });
8  const datos = await r.json();
9
10  await AsyncStorage.setItem("token", datos.token);           // (1)
11  await AsyncStorage.setItem("password", password);           // (2)
12  await AsyncStorage.setItem("usuario", JSON.stringify({
13    id: datos.id,
14    correo,
15    tarjeta: datos.tarjetaGuardada,                           // (3)
16  }));
17}
18
19export async function estaLogueado() {
20  const token = await AsyncStorage.getItem("token");
21  return token !== null;                                      // (4)
22}
23
24export async function logout() {
25  await AsyncStorage.removeItem("token");                     // (5)
26}

Preguntas:

  1. El teléfono del usuario se pierde y alguien saca un respaldo con las herramientas del sistema. ¿Qué obtiene?
  2. ¿Por qué se guarda la contraseña? ¿Hay alguna justificación válida?
  3. estaLogueado() solo revisa que el token exista. ¿Qué pasa cuando el token expiró hace tres días?
  4. Después de logout(), ¿qué queda en el dispositivo?
  5. Reescribe las cuatro funciones.

Documentación Oficial

DocumentaciónPrincipiante15 min

Documentación de apoyo

Las APIs oficiales de esta clase.