Instalación de Async Storage para persistencia de datos en React Native

Clase 14 de 23Curso de Fundamentos de React Native

Resumen

Para que tus hábitos no se pierdan al cerrar la app, aquí verás cómo integrar persistencia con Async Storage, ordenar tus types, crear utils de fecha en date.ts y montar una arquitectura con context y reducer para hidratar, agregar y hacer toggle de hábitos con control de racha. Todo claro, modular y listo para escalar.

¿Cómo instalar y preparar Async Storage?

Antes de guardar datos localmente, instala la librería Async Storage. Asegúrate de crear una Storage Key clara y una versión para futuras migraciones.

  • Instala la librería según el recurso compartido.
  • Define una llave de almacenamiento: STORAGE_KEY = "Hábitos".
  • Define una versión: STORAGE_VERSION = 1.
  • Usa la llave para leer, escribir y actualizar datos.
  • Mantén el state inicial con: habits vacíos y loading: true.

Código orientativo:

// storage.ts
export const STORAGE_KEY = 'Hábitos';
export const STORAGE_VERSION = 1;

¿Qué utilidades de fecha necesitas en date.ts?

Centraliza la lógica de fechas en un util date.ts para reutilizar y tipar mejor tus operaciones: primer día, mismo día, día de ayer y formato ISO.

  • Primer día: normaliza la fecha al inicio del día.
  • Mismo día: compara dos fechas sin horas.
  • Día de ayer: calcula la referencia del día anterior.
  • ISO: guarda y compara en formato estándar de JavaScript.

Código orientativo:

// utils/date.ts
export const startOfDay = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDate());
export const isSameDate = (a: Date, b: Date) => startOfDay(a).getTime() === startOfDay(b).getTime();
export const yesterday = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDate() - 1);
export const toISO = (d: Date) => startOfDay(d).toISOString();

¿Cómo organizar types y el modelo de hábito?

Separa tus modelos y tipos en una carpeta llamada types. Así el código en TSX/JSX queda limpio y escalable.

  • Crea la carpeta: types.
  • Define la prioridad con tres valores conocidos del proyecto.
  • Define el hábito con campos mínimos y fechas para la racha.
  • Incluye identificador único para cada hábito.

Código orientativo:

// types/habits.ts
export type Priority = string; // tres tipos ya definidos en el proyecto.

export interface Habit {
  id: string;
  title: string;
  priority: Priority;
  streak: number; // racha.
  createdAtISO: string; // fecha de creación en ISO.
  lastDoneISO?: string; // última vez completado en ISO.
}

¿Cómo diseñar context y reducer para hábitos?

Crea una carpeta para tus contexts. Ahí vivirá la “central de mando” con state, acciones y reducer para hidratar, agregar y hacer toggle. Controla loading, mapea hábitos y actualiza la racha con fechas.

  • Estado inicial: loading: true, habits: [].
  • Acciones con payload tipado: hydrate, add, toggle.
  • Usa un switch por action.type.
  • En hydrate: loading = false y hábitos desde el payload.
  • En add: crea id único con la fecha y guarda en ISO.
  • En toggle: valida si ya se hizo hoy, calcula racha con “ayer”, y actualiza fechas.
// context/habits.tsx
import { Habit, Priority } from '../types/habits';
import { toISO, isSameDate, yesterday } from '../utils/date';

type HabitsState = { loading: boolean; habits: Habit[] };

type HydrateAction = { type: 'hydrate'; payload: Habit[] };
type AddAction = { type: 'add'; payload: { title: string; priority: Priority } };
type ToggleAction = { type: 'toggle'; payload: { id: string; todayISO: string } };

type HabitsAction = HydrateAction | AddAction | ToggleAction;

export const initialState: HabitsState = { loading: true, habits: [] };

export function habitsReducer(state: HabitsState, action: HabitsAction): HabitsState {
  switch (action.type) {
    case 'hydrate': {
      return { loading: false, habits: action.payload };
    }
    case 'add': {
      const { title, priority } = action.payload;
      const now = new Date();
      const newHabit: Habit = {
        id: now.getTime().toString(),
        title,
        priority,
        streak: 0,
        createdAtISO: toISO(now),
        lastDoneISO: undefined,
      };
      return { ...state, habits: [newHabit, ...state.habits] };
    }
    case 'toggle': {
      const { id, todayISO } = action.payload;
      const today = new Date(todayISO);
      const yest = yesterday(today);

      const updated = state.habits.map(h => {
        if (h.id !== id) return h;

        const doneToday = h.lastDoneISO && isSameDate(new Date(h.lastDoneISO), today);
        if (doneToday) {
          const newStreak = Math.max(0, (h.streak ?? 0) - 1);
          return { ...h, streak: newStreak, lastDoneISO: undefined };
        }

        let newStreak = 1;
        if (h.lastDoneISO && isSameDate(new Date(h.lastDoneISO), yest)) {
          newStreak = (h.streak ?? 0) + 1;
        }
        return { ...h, streak: newStreak, lastDoneISO: toISO(today) };
      });

      return { ...state, habits: updated };
    }
    default:
      return state;
  }
}

¿Qué acciones maneja el reducer y su payload?

  • hydrate: recibe hábitos y desactiva loading.
  • add: usa title y priority del payload.
  • toggle: requiere id y el día de hoy para validar fechas.

¿Cómo actualiza el toggle la racha y las fechas?

  • Si ya está hecho hoy: deselecciona y resta 1 a la racha.
  • Si se completó ayer: suma 1 a la racha.
  • Si no, inicia la racha en 1.
  • Actualiza lastDoneISO con la fecha actual en ISO.

¿Te gustaría ver el provider para conectar esta lógica con la UI y Async Storage? Cuéntame en los comentarios qué parte quieres profundizar primero.