10 exemplos para você começar com dados móveis offline-first - 7 básicos e 3 intermediários. Usuários de dispositivos móveis perdem o sinal constantemente; trate a conectividade como um estado de recurso, não uma exceção.
Crie um aplicativo Expo com formato de produção e um pino explícito de SDK 57. Os exemplos offline assumem Expo Router, TypeScript e TanStack Query como a camada de estado do servidor.
O offline-first começa no produto e na arquitetura - não em um hook NetInfo adicionado após o lançamento. Documente a suposição no README da sua camada de dados ou ADR.
Suposições de offline-first (contrato da equipe):1. Leituras são bem-sucedidas do cache local mesmo quando isConnected === false2. Gravações são aceitas localmente imediatamente; a confirmação do servidor é assíncrona3. Conflitos são resolvidos por uma estratégia documentada (servidor vence, LWW ou CRDT)4. Os usuários sempre sabem se os dados estão ao vivo, desatualizados ou pendentes de sincronização5. A inicialização a frio deve renderizar o último estado conhecido bom dentro do orçamento de um quadro, quando possível
Pause as novas tentativas de consulta quando o dispositivo estiver offline - novas tentativas ilimitadas em modo avião consomem bateria e sobrecarregam os logs.
// src/lib/setupMobileQuery.tsimport NetInfo from "@react-native-community/netinfo";import { AppState, type AppStateStatus } from "react-native";import { focusManager, onlineManager } from "@tanstack/react-query";export function setupMobileQuery() { onlineManager.setEventListener((setOnline) => NetInfo.addEventListener((state) => { const online = !!state.isConnected && state.isInternetReachable !== false; setOnline(online); }) ); const onChange = (status: AppStateStatus) => { focusManager.setFocused(status === "active"); }; const sub = AppState.addEventListener("change", onChange); return () => sub.remove();}
// app/_layout.tsx (trecho)import { QueryClientProvider } from "@tanstack/react-query";import { useEffect, useState } from "react";import { createQueryClient } from "@/lib/queryClient";import { setupMobileQuery } from "@/lib/setupMobileQuery";export default function RootLayout() { const [queryClient] = useState(() => createQueryClient()); useEffect(() => setupMobileQuery(), []); return ( <QueryClientProvider client={queryClient}> {/* rotas */} </QueryClientProvider> );}
onlineManager controla se o Query considera o cliente online para novas tentativas e refetchOnReconnect
focusManager vincula refetchOnWindowFocus ao AppState - refetch quando o aplicativo retorna ao primeiro plano
Chame setupMobileQuery() uma vez na inicialização - ouvintes duplicados causam alternância de estado online
Quando offline, o aplicativo ainda deve aceitar a intenção do usuário - armazene localmente e exiba o status de sincronização.
// src/data/outbox.tsimport AsyncStorage from "@react-native-async-storage/async-storage";export type OutboxItem = { id: string; type: "create_note"; payload: { text: string }; createdAt: number; idempotencyKey: string;};const OUTBOX_KEY = "sync:outbox:v1";export async function enqueueOutbox(item: OutboxItem) { const raw = await AsyncStorage.getItem(OUTBOX_KEY); const queue: OutboxItem[] = raw ? JSON.parse(raw) : []; queue.push(item); await AsyncStorage.setItem(OUTBOX_KEY, JSON.stringify(queue));}export async function readOutbox(): Promise<OutboxItem[]> { const raw = await AsyncStorage.getItem(OUTBOX_KEY); return raw ? JSON.parse(raw) : [];}
// indicador de UI em um compositor de notas{pendingSync && ( <Text style={{ color: "#6b7280" }}>Salvo no dispositivo - será sincronizado quando online</Text>)}
Cada item enfileirado precisa de uma idempotencyKey - reproduções após reconexão não devem duplicar linhas do servidor
O esquema do outbox é versionado (sync:outbox:v1) - migrações seguem Padrões AsyncStorage
Conecte conectividade, cache, armazenamento e UX em uma lista de verificação que as equipes podem copiar.
// src/features/checklist/OfflineChecklistScreen.tsximport { useQuery } from "@tanstack/react-query";import { Switch, Text, View } from "react-native";import { DataFreshnessLabel } from "@/components/DataFreshnessLabel";import { OfflineBanner } from "@/components/OfflineBanner";import { useEffectiveConnectivity } from "@/hooks/useEffectiveConnectivity";import { useOfflineModeStore } from "@/stores/offlineModeStore";import { fetchChecklist } from "@/api/checklist";export function OfflineChecklistScreen() { const { isOffline, forcedOffline } = useEffectiveConnectivity(); const setForcedOffline = useOfflineModeStore((s) => s.setForcedOffline); const { data, isFetching, isPending, dataUpdatedAt, refetch } = useQuery({ queryKey: ["checklist"], queryFn: fetchChecklist, staleTime: 5 * 60_000, // Pula a nova busca de rede no modo offline forçado; dados em cache ainda são renderizados via SWR enabled: !forcedOffline, }); return ( <View style={{ flex: 1, padding: 16 }}> <OfflineBanner /> <View style={{ flexDirection: "row", alignItems: "center", marginBottom: 12 }}> <Text style={{ flex: 1 }}>Forçar modo offline</Text> <Switch value={forcedOffline} onValueChange={setForcedOffline} /> </View> {data && ( <DataFreshnessLabel dataUpdatedAt={dataUpdatedAt} isFetching={isFetching} isOffline={isOffline} /> )} {isPending && !data && <Text>Carregando lista de verificação…</Text>} {!data && isOffline && ( <Text onPress={() => refetch()}>Nenhuma lista de verificação salva. Conecte-se para baixar.</Text> )} {data?.map((item) => ( <Text key={item.id}>• {item.label}</Text> ))} </View> );}
Lista de verificação de leitura offline:✓ NetInfo + onlineManager conectados na inicialização✓ staleTime configurado por sensibilidade da tela✓ Dados em cache mostrados com rótulo de frescura✓ Erros offline vs servidor classificados✓ Gravações enfileiradas localmente com chaves de idempotência✓ Cache de consulta persistido para inicialização a frio (opcional)✓ Modo offline forçado para testes de campo (opcional)
Esta tela é um modelo - substitua fetchChecklist pela sua API de domínio
Adicione dados offline relacionais em SQLite quando as listas excederem o conforto do AsyncStorage - veja expo-sqlite