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
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:
- Preferencias — tema, idioma, filtros, orden. Pequeñas, no sensibles, se leen al arrancar. →
AsyncStorage. - 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. - 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
awaites 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.stringifyal guardar yJSON.parseal leer. Y al leer siempre puede venirnull(la primera vez no hay nada), así que siempre defines un valor por defecto.
Lo que casi nadie te dice de AsyncStorage
🚨
AsyncStorageno 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.
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 entraSecureStore — para lo que no puede filtrarse
Keychain en iOS, Keystore en Android. Cifrado por el sistema operativo.
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:
13• Pensado 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.
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 abrirUn hook para no repetir el patrón
Cargar al montar, guardar al cambiar, con estado de carga honesto.
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á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:
- 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. - 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.
- 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. - 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
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é:
- El tema visual elegido por el usuario (claro/oscuro)
- El token de sesión (JWT)
- La contraseña del usuario
- El último resultado de búsqueda, para mostrarlo al reabrir
- El número de tarjeta con el que pagó
- Un borrador del formulario que estaba llenando cuando entró una llamada
- El id del usuario
- 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 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.
jsx1import 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:
- El teléfono del usuario se pierde y alguien saca un respaldo con las herramientas del sistema. ¿Qué obtiene?
- ¿Por qué se guarda la contraseña? ¿Hay alguna justificación válida?
estaLogueado()solo revisa que el token exista. ¿Qué pasa cuando el token expiró hace tres días?- Después de
logout(), ¿qué queda en el dispositivo? - Reescribe las cuatro funciones.
Documentación Oficial
Documentación de apoyo
Las APIs oficiales de esta clase.