Context y Provider para estado global de hábitos

Clase 15 de 23Curso de Fundamentos de React Native

Resumen

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 orientativo
import React, { createContext, useReducer, useEffect } from 'react';

type HabitsContextType = {
  habits: any[];
  loading: boolean;
  addHabit: (...args: any[]) => void;
  selectHabit: (...args: any[]) => void;
};

const HabitsContext = createContext<HabitsContextType | null>(null);

function HabitsProvider({ children }: { children: React.ReactNode }) {
  const [state, dispatch] = useReducer(reducer, initialState);

  // acciones se implementan más abajo en el flujo
  const addHabit = (...args: any[]) => dispatch({ type: 'add', payload: args });
  const selectHabit = (...args: any[]) => dispatch({ type: 'select', payload: args });

  return (
    <HabitsContext.Provider value={{ 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 = await AsyncStorage.getItem(storageKey);
      if (!raw) return; // sin datos, no se hidrata
      const 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 falla
      dispatch({ 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 hidrata

  if (saveTimer.current) clearTimeout(saveTimer.current);

  saveTimer.current = setTimeout(async () => {
    try {
      await AsyncStorage.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.