TanStack Query en Mobile
focusManager, onlineManager, y refetch en primer plano de la app - el cableado específico mobile que TanStack Query necesita más allá de los tutoriales web.
Busca en todas las páginas de la documentación
focusManager, onlineManager, y refetch en primer plano de la app - el cableado específico mobile que TanStack Query necesita más allá de los tutoriales web.
Tarjeta de receta de referencia rápida - lista para copiar y pegar.
// src/lib/queryClient.ts
import NetInfo from "@react-native-community/netinfo";
import { QueryClient, focusManager, onlineManager } from "@tanstack/react-query";
import { AppState, Platform } from "react-native";
// Refetch cuando la app vuelve al primer plano
focusManager.setEventListener((handleFocus) => {
const sub = AppState.addEventListener("change", (state) => {
if (state === "active") handleFocus();
});
return () => sub.remove();
});
// Pausa reintentos cuando está offline; reanuda cuando se conecta
onlineManager.setEventListener((setOnline) => {
return NetInfo.addEventListener((state) => {
const online =
state.isConnected !== false && state.isInternetReachable !== false;
setOnline(online);
});
});
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 2,
staleTime: 30_000,
gcTime: 24 * 60 * 60_000,
refetchOnWindowFocus: true,
refetchOnReconnect: true,
},
},
});// app/_layout.tsx
import { QueryClientProvider } from "@tanstack/react-query";
import { queryClient } from "../src/lib/queryClient";
export default function RootLayout() {
return (
<QueryClientProvider client={queryClient}>
{/* routes */}
</QueryClientProvider>
);
}npx expo install @tanstack/react-query @react-native-community/netinfoCuándo usarlo:
import NetInfo from "@react-native-community/netinfo";
import {
QueryClient,
QueryClientProvider,
focusManager,
onlineManager,
useQuery,
} from "@tanstack/react-query";
import { useEffect } from "react";
import {
AppState,
FlatList,
Pressable,
RefreshControl,
StyleSheet,
Text,
View,
} from "react-native";
const API = "https://jsonplaceholder.typicode.com";
focusManager.setEventListener((handleFocus) => {
const sub = AppState.addEventListener("change", (s) => {
if (s === "active") handleFocus();
});
return () => sub.remove();
});
onlineManager.setEventListener((setOnline) =>
NetInfo.addEventListener((state) => {
setOnline(!!state.isConnected && state.isInternetReachable !== false);
})
);
const queryClient = new QueryClient({
defaultOptions: {
queries: { staleTime: 45_000, retry: 2, refetchOnReconnect: true },
},
});
async function fetchPosts({ signal }: { signal?: AbortSignal }) {
const res = await fetch(`${API}/posts?_limit=12`, { signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json() as Promise<{ id: number; title: string }[]>;
}
function PostsScreen() {
const { data, error, isError, isFetching, refetch, dataUpdatedAt } = useQuery({
queryKey: ["posts"],
queryFn: fetchPosts,
});
return (
<View style={styles.screen}>
<Text style={styles.meta}>
{isFetching ? "Actualizando…" : `Actualizado ${new Date(dataUpdatedAt).toLocaleTimeString()}`}
</Text>
<FlatList
data={data}
keyExtractor={(item) => String(item.id)}
refreshControl={<RefreshControl refreshing={isFetching} onRefresh={refetch} />}
renderItem={({ item }) => <Text style={styles.row}>{item.title}</Text>}
ListEmptyComponent={isError ? null : <Text>Cargando…</Text>}
/>
{isError && (
<View style={styles.error}>
<Text>{error instanceof Error ? error.message : "Falló"}</Text>
<Pressable onPress={() => refetch()}>
<Text style={styles.retry}>Reintentar</Text>
</Pressable>
</View>
)}
</View>
);
}
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<PostsScreen />
</QueryClientProvider>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, padding: 16, backgroundColor: "#fff" },
meta: { fontSize: 12, color: "#64748b", marginBottom: 8 },
row: { paddingVertical: 10, borderBottomWidth: StyleSheet.hairlineWidth, borderColor: "#e2e8f0" },
error: { padding: 12, backgroundColor: "#fee2e2", borderRadius: 8 },
retry: { color: "#2563eb", fontWeight: "600", marginTop: 8 },
});Lo que esto demuestra:
focusManager + AppState hace refetch de queries obsoletas cuando la app se vuelve active.onlineManager + NetInfo pausa reintentos offline y dispara refetchOnReconnect cuando LTE vuelve.signal en queryFn cancela fetch cuando la query se desmonta o la clave cambia.refetch() - UX mobile idiomática junto con refetch en primer plano.dataUpdatedAt etiqueta la obsolescencia para usuarios en redes inestables.| AppState | Comportamiento de TanStack |
|---|---|
active | handleFocus() - refetch de queries obsoletas si refetchOnWindowFocus |
background | Las queries se mantienen en caché; las solicitudes en vuelo pueden completarse |
inactive (iOS) | Tratar como una breve interrupción - el mismo listener |
// Opcional: solo hace refetch de queries "importantes" en focus
queryClient.refetchQueries({ queryKey: ["inbox"], type: "active" });staleTime: 0 solo para esas clavesqueryKeyonlineManager.setEventListener((setOnline) => {
return NetInfo.addEventListener((state) => {
// null reachability = desconocido - no fuerces offline
if (state.isConnected === false) {
setOnline(false);
return;
}
if (state.isInternetReachable === false) {
setOnline(false);
return;
}
setOnline(true);
});
});refetchOnReconnect: true (predeterminado) hace refetch de queries obsoletas cuando onlineManager cambia a truenetworkMode: "offlineFirst" solo cuando tienes una cola persistida - ver Offline-First Basics| Opción | Valor mobile sugerido | Por qué |
|---|---|---|
staleTime | 30-60 s feeds; 5-15 min catálogos | Reduce charla de LTE |
gcTime | 24 h | Sobrevive sesiones offline cortas |
retry | 2 | Parpadeos de LTE se recuperan; límite evita agotamiento de batería |
retryDelay | exponencial 1s - 4s | Ver doc de Retries |
refetchOnWindowFocus | true con focusManager | Mobile "window" = app en primer plano |
refetchOnReconnect | true con onlineManager | Ponerse al día después del metro |
npx expo install @tanstack/react-query-persist-client @react-native-async-storage/async-storageimport AsyncStorage from "@react-native-async-storage/async-storage";
import { createAsyncStoragePersister } from "@tanstack/query-async-storage-persister";
import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client";
const persister = createAsyncStoragePersister({ storage: AsyncStorage });
// Envuelve QueryClientProvider con PersistQueryClientProvider en root layoutuseQuery({
queryKey: ["profile", userId],
queryFn: fetchProfile,
enabled: !!userId,
});enabled: false hasta que existe sesión - evita tormentas 401 en el lanzamiento["profile"] en logout: queryClient.clear() o removeQueriesqueryFn vía cliente compartido - no dentro de cada hookAppState en arranque de app.isInternetReachable: null como offline - Banner y parpadeo de reintento en el lanzamiento. Solución: Solo establece offline en false explícito.signal en queryFn - Las solicitudes se completan después del desmontaje. Solución: fetch(url, { signal }) con signal inyectado de Query.staleTime: 0 globalmente - Refetch en cada focus agota la batería. Solución: Ajuste por query.| Alternativa | Usar cuando | No usar cuando |
|---|---|---|
| TanStack Query + managers | Estado de servidor predeterminado para Expo | Estado local puro sin red |
| SWR | Equipo ya en SWR desde web | Greenfield Expo - Query tiene docs RN más ricas en este sitio |
| Redux + thunks | Tienda heredada ya centralizada | Nuevas apps - lógica de caché duplicada |
| Fetch manual con useEffect | Una pantalla, sin caché compartida | Múltiples pantallas leyendo la misma API |
Una vez en carga de módulo en queryClient.ts o root _layout.tsx antes de que se ejecuten queries - no dentro de pantallas individuales.
No - React Native no tiene eventos de window focus. Debes conectar AppState a focusManager.
Solo mutations idempotentes con claves del lado del servidor. retry: 0 predeterminado para pagos POST - ver Retries, Backoff & Idempotency.
queryClient.prefetchQuery({ queryKey: ["post", id], queryFn: () => fetchPost(id) });Llama en onPressIn de filas de lista para navegación ágil.
TanStack Query proporciona stale-while-revalidate y persistencia opcional. offline-first completo añade sincronización de SQLite - Offline-First Basics.
queryFnretryDelay y seguridad de mutationuseMutation en dispositivoenabled y limpieza de caché en logoutStack versions: Esta página fue escrita para React 19.2.3, React Native 0.86.0, y Expo SDK 57 (
expo~57.0.4).
Revisado por Chris St. John·Última actualización: 16 jul 2026