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.
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.
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
- Planificación de la arquitectura y la pila tecnológica
- Flujo de autenticación y rutas protegidas
- Función principal: feed de datos con compatibilidad sin conexión
- Perfeccionamiento, pruebas y publicación en ambas tiendas