Crear un estado global de hábitos confiable en React es más sencillo si organizas bien el context, el provider y las actualizaciones con useReducer y useEffect. Aquí verás cómo hidratar el estado desde AsyncStorage, cómo persistir cambios sin errores y cómo preparar las acciones de agregar y seleccionar hábitos. Todo, paso a paso, con buenas prácticas y mensajes de control en consola.
¿Cómo definir el context y el provider en React?
Para centralizar la lógica, se define un tipo que agrupe hábitos, estado de carga y funcionalidades. Luego, se crea el context con ese tipo y un valor por defecto nulo si no está disponible. El provider expone el estado y las funciones, devolviendo un ReactNode.
Define un tipo con hábitos, loading y funciones de negocio.
Crea el context con createContext y usa el tipo o null como valor inicial.
Implementa el provider que retornará el Provider del contexto.
Integra useReducer con un reducer y un initialState.
// Esquema orientativoimportReact,{ createContext, useReducer, useEffect }from'react';typeHabitsContextType={ habits:any[]; loading:boolean;addHabit:(...args:any[])=>void;selectHabit:(...args:any[])=>void;};constHabitsContext=createContext<HabitsContextType|null>(null);functionHabitsProvider({ children }:{ children:React.ReactNode}){const[state, dispatch]=useReducer(reducer, initialState);// acciones se implementan más abajo en el flujoconstaddHabit=(...args:any[])=>dispatch({ type:'add', payload: args });constselectHabit=(...args:any[])=>dispatch({ type:'select', payload: args });return(<HabitsContext.Providervalue={{ habits: state.habits, loading: state.loading, addHabit, selectHabit }}>{children}</HabitsContext.Provider>);}
Clave: el useReducer retorna el estado y una función de actualización (dispatch), que permite ejecutar acciones con su payload.
¿Cómo hidratar el estado con AsyncStorage y useEffect?
La hidratación carga los hábitos guardados en la “memoria de la aplicación” con AsyncStorage. Se usa useEffect asíncrono, con try/catch, para leer por la clave definida (por ejemplo, storageKey), parsear el JSON y despachar la acción de hidratar.
Usa un useEffect inicial para leer del almacenamiento.
Llama a getItem con la key definida: storageKey.
Convierte el “raw” a objeto con JSON.parse.
Despacha { type: 'hydrate', payload } con los datos.
Maneja errores con console.warn: "No se pudo cargar hábitos".
useEffect(()=>{(async()=>{try{const raw =awaitAsyncStorage.getItem(storageKey);if(!raw)return;// sin datos, no se hidrataconst data =JSON.parse(raw);dispatch({ type:'hydrate', payload: data });}catch(e){console.warn('No se pudo cargar hábitos');// opcional: asegurar un estado válido si algo falladispatch({ type:'hydrate', payload:[]});}})();},[]);
Importante: si no existe el tipo o la data, se devuelve null o un estado vacío, evitando rupturas en la UI.
¿Cómo guardar cambios y sincronizar hábitos sin errores?
Cada vez que cambian los hábitos, se debe persistir el estado. Para evitar escrituras redundantes, se usa un temporizador de guardado (save timer) y se limpia con clearTimeout antes de programar un nuevo guardado. Además, si el estado está loading, se evita guardar.
Omite el guardado si state.loading es true.
Usa un temporizador para controlar escrituras.
Limpia con clearTimeout antes de programar otra operación.
Guarda con AsyncStorage.setItem y JSON.stringify del estado.
Controla errores: console.warn("No se pudo guardar").
Devuelve una limpieza que cancele el temporizador activo.
const saveTimer =React.useRef<ReturnType<typeof setTimeout>|null>(null);useEffect(()=>{if(state.loading)return;// no guardar mientras hidrataif(saveTimer.current)clearTimeout(saveTimer.current); saveTimer.current=setTimeout(async()=>{try{awaitAsyncStorage.setItem(storageKey,JSON.stringify(state.habits));}catch(e){console.warn('No se pudo guardar');}});return()=>{if(saveTimer.current)clearTimeout(saveTimer.current);};},[state.habits, state.loading]);
¿Qué acciones del reducer se exponen en el provider?
Al final, el provider expone las funciones conectadas al dispatch para operar sobre el estado global.
Acción "hydrate": repuebla el estado con los hábitos cargados.
Acción "add": agrega un hábito al listado.
Acción "select": marca o selecciona un hábito.
Resultado: el context actúa como “centro de mando”, entregando estado y funcionalidades listas para usar en la aplicación.
¿Te gustaría ver cómo implementar y consumir este provider en tus componentes? Comparte tus dudas o casos y seguimos construyendo juntos.
Debes "hidratar" tu estado global exactamente en el momento en que la aplicación se monta por primera vez en la pantalla del usuario. Piensa en la hidratación como el proceso de despertar a tu aplicación y darle su café matutino: necesita recuperar su memoria antes de empezar a trabajar.
Para lograr esto, utilizamos un useEffect sin dependencias (o que se ejecute una sola vez) que vaya directamente a la memoria local del dispositivo mediante AsyncStorage. Como los datos guardados allí están en formato de texto plano (un string crudo), este paso toma esa información, la transforma de vuelta a un objeto estructurado usando JSON.parse(), y finalmente inyecta esos datos en tu estado global. Si omites este paso, tus usuarios verían una pantalla vacía cada vez que abren la app, perdiendo todo su progreso anterior.
Se usa useRef para saveTimer por tres razones principales:
1. Persistencia entre renders
Una variable normal (let saveTimer) se recrearía en cada render, perdiendo la referencia al timeout anterior. useRef mantiene el mismo valor entre renders.
2. No causar re-renders
Si usaras useState, cada vez que cambies el timer se dispararía un re-render innecesario. useRef permite mutar el valor sin causar re-renders.
Necesitas acceder al timeout anterior para cancelarlo antes de crear uno nuevo. Si cada render creara una nueva variable, no podrías limpiar los timeouts pendientes, causando múltiples guardados simultáneos.
Comparación:
// ❌ Variable normal - se pierde en cada renderlet saveTimer =null;// ❌ useState - causa re-renders innecesariosconst[saveTimer, setSaveTimer]=useState(null);// ✅ useRef - persiste SIN re-rendersconst saveTimer = useRef<NodeJS.Timeout|null>(null);
En resumen: useRef es perfecto para valores mutables que no afectan la UI (timers, referencias DOM, contadores internos, etc.)
Que buen aporte Nicolas!
Sabes por que se coloca 2 veces el ?
if (saveTimer.current) clearTimeout(saveTimer.current);
¿Qué pasa si falla el AsyncStorage?
Si la memoria local del dispositivo falla al intentar leer o escribir datos, tu aplicación podría colapsar repentinamente, dejando al usuario frente a una pantalla congelada o un cierre inesperado. Esto suele ocurrir por falta de espacio en el dispositivo o permisos denegados.
Para blindar tu código contra estos desastres, es crucial envolver las llamadas asíncronas dentro de un bloque try...catch. Si el AsyncStorage falla al recuperar la información, el bloque catch captura el error silenciosamente. En ese escenario, tu deber es retornar un estado inicial vacío o un valor null controlado, permitiendo que la aplicación siga funcionando como si fuera la primera vez que el usuario la abre. Además, registrar el error con un console.warn te dará visibilidad como desarrollador para investigar el problema sin arruinar la experiencia del usuario final.
¿Soy el único que no está entendiendo nada? Está muy mal explicado este curso. ¿De dónde salen las cosas? ¿Qué es el concepto de hidratar? ¿Qué es un context? ¿Por qué el provider está dentro del context? Siento que faltan muchas explicaciones, es solo una descripción lineal del código, sin explicar ningún concepto. No digo de los useEffect, useRef, useState, porque esas son herramientas de React que se explican en otros cursos y se pueden aprender allí, pero hay cosas centrales de este curso que jamás se explican, sólo se muestra cómo se hace (cuando no se copia y pega código magicamente de andá a saber dónde...), además es de los pocos cursos de Platzi que no tienen el código subido a algún repositorio como para ir revisando los commits o el mismo código.