React Native Academy · Lección

Función principal: feed de datos con compatibilidad sin conexión

Construya el feed principal de datos con React Query y persistencia sin conexión, implemente la actualización mediante deslizamiento y la paginación, añada mutaciones de creación y eliminación, y anime la entrada de los elementos de la lista.

Lección 3 de 413 pasos

Función principal: feed de datos con compatibilidad sin conexión es una lección gratuita de React Native Academy en CoddyKit. Esta es la lección 3 de 4. Puedes leer la lección completa abajo gratuitamente — luego la practicas en el navegador con un editor de código integrado y un tutor de IA 24/7. Forma parte de la ruta de aprendizaje de React Native Academy, y tu progreso se sincroniza en la web y la app de CoddyKit. El curso de React Native Academy incluye 4 lecciones en total.

Qué es el diseño offline-first

El diseño offline-first significa que la aplicación funciona correctamente sin conexión a Internet: los usuarios pueden ver datos en caché, realizar mutaciones que se ponen en cola para sincronizarse y no reciben mensajes de error simplemente porque la red no está disponible. Los usuarios de dispositivos móviles entran y salen de zonas con conectividad con frecuencia. Una aplicación de React Native que deja de funcionar sin red parece frágil; una que sigue funcionando transmite solidez y fiabilidad.

Configuración de QueryClient

El QueryClient de React Query es la base de la capa de datos. Configúrelo con valores predeterminados razonables: un staleTime que evite volver a obtener datos innecesariamente (por ejemplo, 5 minutos para datos que cambian con poca frecuencia) y un gcTime (tiempo de recolección de basura) que mantenga los datos no utilizados en la caché el tiempo suficiente para servirlos desde ella al recuperar la conexión. Pase este cliente a QueryClientProvider en la raíz de la aplicación.

// src/services/queryClient.ts
import { QueryClient } from '@tanstack/react-query';

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5,  // 5 minutes — don't refetch too often
      gcTime: 1000 * 60 * 60,    // Keep in cache 1 hour after unmount
      retry: 2,
      retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000),
      refetchOnWindowFocus: false, // Not needed on mobile
      refetchOnReconnect: true,    // Sync when network returns
    },
  },
});

Persistencia de la caché de consultas con AsyncStorage

Para que los datos estén disponibles sin conexión, persista la caché de React Query en AsyncStorage. Use @tanstack/react-query-persist-client con un persister de AsyncStorage. Al iniciar la aplicación, la caché persistida se restaura antes de la primera solicitud de red, por lo que los usuarios ven inmediatamente su lista de hábitos, incluso antes de que finalice la obtención. La caché se hidrata de forma asíncrona; por eso, muestre los datos en caché de forma optimista y actualícelos cuando lleguen los datos recientes.

// App.tsx
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { queryClient } from './src/services/queryClient';

const asyncStoragePersister = createAsyncStoragePersister({
  storage: AsyncStorage,
  key: 'REACT_QUERY_CACHE',
});

export default function App() {
  return (
    <PersistQueryClientProvider
      client={queryClient}
      persistOptions={{ persister: asyncStoragePersister }}
    >
      <AuthProvider>
        <RootNavigator />
      </AuthProvider>
    </PersistQueryClientProvider>
  );
}

Obtener la lista de hábitos con useQuery

Cree un hook personalizado useHabits que encapsule la consulta de Supabase dentro de useQuery. La clave de consulta ['habits', userId] limita la caché al usuario actual y evita que los datos se filtren entre cuentas. React Query devuelve data, isLoading y error; pase los tres valores a la pantalla para mostrar el estado correcto de la interfaz en cada momento.

// src/hooks/useHabits.ts
import { useQuery } from '@tanstack/react-query';
import { supabase } from '../services/supabase';
import { useAuth } from '../context/AuthContext';
import { Habit } from '../types';

async function fetchHabits(userId: string): Promise<Habit[]> {
  const { data, error } = await supabase
    .from('habits')
    .select('*')
    .eq('user_id', userId)
    .order('created_at', { ascending: true });

  if (error) throw error;
  return data;
}

export function useHabits() {
  const { session } = useAuth();
  const userId = session!.user.id;

  return useQuery({
    queryKey: ['habits', userId],
    queryFn: () => fetchHabits(userId),
    enabled: !!userId,
  });
}

Renderizar FlatList con datos en caché

Utilice el hook useHabits en la pantalla HabitList. Muestre un cargador de tipo skeleton o un indicador durante la carga inicial, use directamente el arreglo data como origen de FlatList y gestione el estado de error con un botón para reintentar. Como la caché se conserva, en los siguientes inicios de la aplicación data estará disponible de inmediato (sin parpadeo de carga), mientras React Query obtiene una versión actualizada en segundo plano.

// src/screens/habits/HabitListScreen.tsx
import { FlatList, ActivityIndicator, View, Text } from 'react-native';
import { useHabits } from '../../hooks/useHabits';
import { HabitCard } from '../../components/HabitCard';

export function HabitListScreen() {
  const { data: habits, isLoading, error, refetch } = useHabits();

  if (isLoading && !habits) {
    return <ActivityIndicator style={{ flex: 1 }} />;
  }

  if (error) {
    return (
      <View>
        <Text>Failed to load habits.</Text>
        <Button title='Retry' onPress={() => refetch()} />
      </View>
    );
  }

  return (
    <FlatList
      data={habits}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => <HabitCard habit={item} />}
      onRefresh={refetch}
      refreshing={isLoading}
    />
  );
}

Crear hábitos con useMutation

useMutation gestiona la operación de inserción. En onSuccess, llame a queryClient.invalidateQueries(['habits', userId]) para activar una nueva obtención de la lista de hábitos. Esto garantiza que el hábito recién creado aparezca en la lista inmediatamente después de crearlo, sin tener que actualizar la caché manualmente. Configure onError para mostrar un mensaje de error al usuario si la inserción falla.

// src/hooks/useCreateHabit.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '../services/supabase';
import { useAuth } from '../context/AuthContext';

export function useCreateHabit() {
  const queryClient = useQueryClient();
  const { session } = useAuth();
  const userId = session!.user.id;

  return useMutation({
    mutationFn: async ({ name, icon, color }) => {
      const { error } = await supabase.from('habits').insert({
        user_id: userId,
        name,
        icon,
        color,
      });
      if (error) throw error;
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['habits', userId] });
    },
    onError: (error) => {
      Alert.alert('Error', error.message);
    },
  });
}

Actualizaciones optimistas para registrar el cumplimiento

Para el registro diario de cumplimiento (marcar un hábito como completado), use actualizaciones optimistas para actualizar la interfaz al instante, antes de que finalice la solicitud de red. Cuando el usuario pulse el botón de registro, actualice inmediatamente la caché con el resultado previsto. Si la solicitud falla, revierta la caché al estado anterior. Así, la aplicación parece responder al instante incluso con redes lentas.

// Optimistic update pattern with useMutation
const toggleCompletion = useMutation({
  mutationFn: (habitId) => insertCompletion(habitId, today),

  onMutate: async (habitId) => {
    // Cancel in-flight refetches
    await queryClient.cancelQueries({ queryKey: ['completions', userId] });

    // Save snapshot for rollback
    const previous = queryClient.getQueryData(['completions', userId]);

    // Optimistically add completion
    queryClient.setQueryData(['completions', userId], (old) => [
      ...old,
      { habit_id: habitId, completed_date: today }
    ]);

    return { previous }; // Pass snapshot to onError
  },

  onError: (_err, _habitId, context) => {
    // Roll back on failure
    queryClient.setQueryData(['completions', userId], context.previous);
  },

  onSettled: () => {
    queryClient.invalidateQueries({ queryKey: ['completions', userId] });
  },
});

Estado de la red y banner sin conexión

Muestre a los usuarios un discreto banner sin conexión cuando el dispositivo no tenga conexión a Internet, para que entiendan por qué sus cambios no se han sincronizado. Use @react-native-community/netinfo para escuchar los cambios de conectividad. React Query pausa automáticamente las mutaciones cuando no hay conexión y las reintenta cuando esta vuelve; el banner simplemente hace visible este comportamiento al usuario.

import NetInfo from '@react-native-community/netinfo';
import { useEffect, useState } from 'react';

export function OfflineBanner() {
  const [isOffline, setIsOffline] = useState(false);

  useEffect(() => {
    const unsubscribe = NetInfo.addEventListener((state) => {
      setIsOffline(!state.isConnected);
    });
    return unsubscribe;
  }, []);

  if (!isOffline) return null;

  return (
    <View style={{
      backgroundColor: '#FF3B30',
      padding: 8,
      alignItems: 'center'
    }}>
      <Text style={{ color: 'white', fontWeight: 'bold' }}>
        No internet connection — changes will sync when online
      </Text>
    </View>
  );
}

Paginación con FlatList y useInfiniteQuery

Para feeds con muchos elementos, implemente la paginación mediante useInfiniteQuery. Este hook obtiene páginas de datos y las añade a una lista acumulada. Pase fetchNextPage al controlador onEndReached de FlatList para que la página siguiente se cargue automáticamente mientras el usuario se desplaza. Use getNextPageParam para obtener el cursor de la página siguiente a partir de la respuesta de la última página.

import { useInfiniteQuery } from '@tanstack/react-query';

export function useHabitsPaginated() {
  return useInfiniteQuery({
    queryKey: ['habits', 'paginated'],
    queryFn: ({ pageParam = 0 }) =>
      supabase.from('habits').select('*')
        .range(pageParam, pageParam + 19),  // 20 per page
    getNextPageParam: (lastPage, allPages) => {
      const nextOffset = allPages.length * 20;
      return lastPage.data?.length === 20 ? nextOffset : undefined;
    },
    initialPageParam: 0,
  });
}

// In screen:
const { data, fetchNextPage, hasNextPage } = useHabitsPaginated();
const habits = data?.pages.flatMap(p => p.data) ?? [];

<FlatList
  data={habits}
  onEndReached={() => hasNextPage && fetchNextPage()}
  onEndReachedThreshold={0.3}
/>

Animar la entrada de elementos de la lista

Haga que el feed resulte más dinámico animando los elementos a medida que aparecen. Una animación sencilla de aparición gradual y deslizamiento hacia arriba al montarse, mediante la Animated API, aporta un acabado cuidado sin necesidad de bibliotecas pesadas. Cree un envoltorio reutilizable AnimatedListItem por el que pase cada elemento. La animación se ejecuta una vez al montarse; use useRef para evitar que se vuelva a activar en nuevas renderizaciones.

import { useRef, useEffect } from 'react';
import { Animated } from 'react-native';

export function AnimatedListItem({ children, index }) {
  const opacity = useRef(new Animated.Value(0)).current;
  const translateY = useRef(new Animated.Value(20)).current;

  useEffect(() => {
    Animated.parallel([
      Animated.timing(opacity, {
        toValue: 1,
        duration: 300,
        delay: index * 50,  // Stagger by index
        useNativeDriver: true,
      }),
      Animated.timing(translateY, {
        toValue: 0,
        duration: 300,
        delay: index * 50,
        useNativeDriver: true,
      }),
    ]).start();
  }, []);

  return (
    <Animated.View style={{ opacity, transform: [{ translateY }] }}>
      {children}
    </Animated.View>
  );
}

Lógica para calcular la racha

La racha es el número de días consecutivos en los que se ha completado un hábito hasta hoy, incluido hoy. Calcúlela en JavaScript a partir del arreglo de cumplimientos. Ordene los cumplimientos por fecha descendente, empiece por hoy y recorra las fechas hacia atrás contando los días consecutivos. Esta lógica es JavaScript puro: no se necesita ninguna llamada de red si los cumplimientos ya están en la caché de React Query.

// src/utils/streaks.ts
export function calculateStreak(completedDates: string[]): number {
  if (completedDates.length === 0) return 0;

  const sorted = [...completedDates].sort().reverse();
  const today = new Date().toISOString().split('T')[0];

  let streak = 0;
  let current = new Date(today);

  for (const dateStr of sorted) {
    const expected = current.toISOString().split('T')[0];
    if (dateStr === expected) {
      streak++;
      current.setDate(current.getDate() - 1);
    } else if (dateStr < expected) {
      break; // Gap found
    }
  }

  return streak;
}

// Usage:
const streak = calculateStreak(
  completions
    .filter(c => c.habit_id === habit.id)
    .map(c => c.completed_date)
);

Comprobación rápida

Compruebe su comprensión de los conceptos de desarrollo móvil con React Native de esta lección.

Resumen de la lección

En esta lección ha aprendido: a configurar React Query con persistencia para servir datos en caché sin conexión, a crear hooks personalizados con useQuery y useMutation para las operaciones CRUD de hábitos y a implementar actualizaciones optimistas para ofrecer una respuesta inmediata en la interfaz. También ha visto cómo añadir paginación, animaciones de entrada y calcular rachas a partir de datos en caché. A continuación, perfeccionaremos la aplicación, la probaremos y la publicaremos en ambas tiendas.

Gratis para empezar

Aprende JavaScript con un tutor de IA — gratis

Escribe y ejecuta código real en tu navegador, obtén ayuda instantánea de un tutor de IA disponible 24/7 y continúa donde lo dejaste en la web o en la aplicación.

Cursos
30
Lecciones
120

Preguntas frecuentes

¿La lección «Función principal: feed de datos con compatibilidad sin conexión» es gratis?

Sí — el texto completo de «Función principal: feed de datos con compatibilidad sin conexión» es gratis para leer aquí en la web. Para practicarla de forma interactiva (editor de código integrado y tutor de IA 24/7) y desbloquear el resto del curso de React Native Academy, actualiza a CoddyKit PRO. El curso de React Native Academy incluye 4 lecciones en total.

¿Qué aprenderé en «Función principal: feed de datos con compatibilidad sin conexión»?

Construya el feed principal de datos con React Query y persistencia sin conexión, implemente la actualización mediante deslizamiento y la paginación, añada mutaciones de creación y eliminación, y ani… Practicas React Native Academy con código real que ejecutas directamente en el navegador, y un tutor de IA 24/7 responde tus preguntas mientras trabajas en la lección.

¿Necesito experiencia previa para empezar React Native Academy?

No se requiere experiencia previa. React Native Academy en CoddyKit está estructurado para principiantes hasta estudiantes avanzados, así que puedes empezar aquí o desde el inicio y avanzar a tu ritmo. Esta es la lección 3 de 4.

¿Cuánto tiempo toma la lección «Función principal: feed de datos con compatibilidad sin conexión»?

La mayoría de las lecciones de CoddyKit toman alrededor de 5–10 minutos. Cada una es compacta e interactiva, así que avanzas constantemente y retomas exactamente por donde dejaste en la web y la app.

¿Puedo escribir y ejecutar código en esta lección de React Native Academy?

Sí. Cada lección de React Native Academy incluye un editor de código integrado, así que escribes y ejecutas código real directamente en tu navegador y obtienes retroalimentación instantánea de IA — sin configuración local necesaria.

Todas las lecciones de este curso

  1. Planificación de la arquitectura y la pila tecnológica
  2. Flujo de autenticación y rutas protegidas
  3. Función principal: feed de datos con compatibilidad sin conexión
  4. Perfeccionamiento, pruebas y publicación en ambas tiendas
← Volver a React Native Academy