TanStack Query on Mobile
focusManager, onlineManager, e refetch na inicialização do app - a configuração específica para mobile que o TanStack Query necessita além dos tutoriais web.
Busque em todas as páginas da documentação
focusManager, onlineManager, e refetch na inicialização do app - a configuração específica para mobile que o TanStack Query necessita além dos tutoriais web.
Cartão de receita de referência rápida - pronto para copiar e colar.
// 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 quando o app retorna para o foreground
focusManager.setEventListener((handleFocus) => {
const sub = AppState.addEventListener("change", (state) => {
if (state === "active") handleFocus();
});
return () => sub.remove();
});
// Pausar retries quando offline; retomar quando conectado
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}>
{/* rotas */}
</QueryClientProvider>
);
}npx expo install @tanstack/react-query @react-native-community/netinfoQuando usar isso:
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 ? "Atualizando…" : `Atualizado ${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>Carregando…</Text>}
/>
{isError && (
<View style={styles.error}>
<Text>{error instanceof Error ? error.message : "Falha"}</Text>
<Pressable onPress={() => refetch()}>
<Text style={styles.retry}>Tentar Novamente</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 },
});O que isso demonstra:
focusManager + AppState refaz o fetch de queries obsoletas quando o app se torna active.onlineManager + NetInfo pausa retries offline e dispara refetchOnReconnect quando o LTE retorna.signal em queryFn cancela o fetch quando a query é desmontada ou a chave muda.refetch() - UX idiomática de mobile junto com refetch em primeiro plano.dataUpdatedAt rotula a obsolescência para usuários em redes instáveis.| AppState | Comportamento do TanStack |
|---|---|
active | handleFocus() - refetch de queries obsoletas se refetchOnWindowFocus |
background | Queries permanecem em cache; requisições em andamento podem completar |
inactive (iOS) | Tratar como interrupção breve - mesmo listener |
// Opcional: refetch apenas queries "importantes" ao focar
queryClient.refetchQueries({ queryKey: ["inbox"], type: "active" });staleTime: 0 apenas para essas chavesqueryKeyonlineManager.setEventListener((setOnline) => {
return NetInfo.addEventListener((state) => {
// null reachability = desconhecido - não forçar offline
if (state.isConnected === false) {
setOnline(false);
return;
}
if (state.isInternetReachable === false) {
setOnline(false);
return;
}
setOnline(true);
});
});refetchOnReconnect: true (padrão) refaz o fetch de queries obsoletas quando onlineManager muda para truenetworkMode: "offlineFirst" apenas quando você tem uma fila persistida - veja Bases Offline-First| Opção | Valor sugerido para mobile | Por quê |
|---|---|---|
staleTime | 30–60 s para feeds; 5–15 min para catálogos | Reduz tráfego LTE |
gcTime | 24 h | Sobrevive a sessões offline curtas |
retry | 2 | Falhas momentâneas de LTE se recuperam; limite evita dreno de bateria |
retryDelay | 1s → 4s exponencial | Veja Documento de Retries |
refetchOnWindowFocus | true com focusManager | "Janela" do mobile = app em primeiro plano |
refetchOnReconnect | true com onlineManager | Atualiza após passar pelo metrô |
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/react-query-async-storage-persister";
import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client";
const persister = createAsyncStoragePersister({ storage: AsyncStorage });
// Envolver QueryClientProvider com PersistQueryClientProvider no layout raizuseQuery({
queryKey: ["profile", userId],
queryFn: fetchProfile,
enabled: !!userId,
});enabled: false até que a sessão exista - evita tempestades de 401 no lançamento["profile"] ao deslogar: queryClient.clear() ou removeQueriesqueryFn via cliente compartilhado - não dentro de cada hookAppState no bootstrap do app.isInternetReachable: null como offline - Banner e retry piscam no lançamento. Correção: Definir offline apenas com false explícito.signal em queryFn - Requisições completam após desmontagem. Correção: fetch(url, { signal }) com o sinal injetado pelo Query.staleTime: 0 globalmente - Refetch a cada foco drena a bateria. Correção: Ajuste por query.| Alternativa | Usar Quando | Não Usar Quando |
|---|---|---|
| TanStack Query + managers | Estado do servidor padrão para Expo | Estado local puro sem rede |
| SWR | Equipe já usa SWR da web | Expo Greenfield - Query tem docs RN mais ricos neste site |
| Redux + thunks | Loja legada já é central | Novos apps - lógica de cache duplicada |
| Fetch manual com useEffect | Uma tela, sem cache compartilhado | Múltiplas telas lendo a mesma API |
Uma vez no carregamento do módulo em queryClient.ts ou no _layout.tsx raiz antes que qualquer query seja executada - não dentro de telas individuais.
Não - React Native não tem eventos de foco de window. Você deve conectar AppState ao focusManager.
Apenas mutações idempotentes com chaves do lado do servidor. Padrão retry: 0 para pagamentos POST - veja Retries, Backoff & Idempotency.
queryClient.prefetchQuery({ queryKey: ["post", id], queryFn: () => fetchPost(id) });Chame em onPressIn de linhas da lista para navegação rápida.
TanStack Query fornece stale-while-revalidate e persistência opcional. Offline-first completo adiciona sincronização SQLite - Bases Offline-First.
queryFnretryDelay e segurança de mutaçãouseMutation no dispositivoenabled e limpeza de cache ao deslogarVersões da Stack: Esta página foi escrita para React 19.2.3, React Native 0.86.0, e Expo SDK 57 (
expo~57.0.4).
Revisado por Chris St. John·Última atualização: 16 de jul. de 2026