Noções Básicas de Rede
10 exemplos de timeouts, cancelamento e análise de dados na borda móvel - a base que todo aplicativo Expo SDK 57 precisa antes de adicionar TanStack Query ou axios.
Busque em todas as páginas da documentação
10 exemplos de timeouts, cancelamento e análise de dados na borda móvel - a base que todo aplicativo Expo SDK 57 precisa antes de adicionar TanStack Query ou axios.
React Native 0.86 inclui uma implementação WHATWG fetch baseada em pilhas de rede nativas. Comece com um aplicativo Expo em branco:
npx create-expo-app@latest MyNetworkApp --template blank-typescript
cd MyNetworkApp
npx expo install zod @react-native-community/netinfoCrie um pequeno módulo de API para que as telas nunca chamem URLs brutas:
// src/api/config.ts
export const API_BASE =
process.env.EXPO_PUBLIC_API_URL ?? "https://jsonplaceholder.typicode.com";Ferramentas: Estes exemplos visam Expo SDK 57 (
expo~57.0.4), React Native 0.86.0 e React 19.2.3. Teste em um dispositivo físico via celular - simuladores ocultam a dor das transições de LTE.
O fetch móvel espelha as APIs da web. Sempre verifique response.ok antes de analisar - um HTTP 404 ainda resolve a promessa.
async function getPost(id: number) {
const res = await fetch(`${API_BASE}/posts/${id}`);
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
return res.json() as Promise<{ id: number; title: string; body: string }>;
}fetch nunca lança erro em 4xx/5xx - você deve inspecionar res.ok ou res.statusTypeError - muitas vezes indistinguíveis de offline à primeira vistaAPI_BASE - nunca espalhe hosts de produção por pastas de recursosRelacionado: Fetch vs axios - quando interceptadores justificam o custo do bundle
Ao contrário dos navegadores, o fetch do React Native não tem opção de timeout embutida. Combine AbortController com setTimeout:
export async function fetchWithTimeout(
input: RequestInfo | URL,
init: RequestInit = {},
timeoutMs = 10_000
) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetch(input, { ...init, signal: controller.signal });
return res;
} finally {
clearTimeout(timer);
}
}// Uso
const res = await fetchWithTimeout(`${API_BASE}/posts/1`, {}, 8_000);AbortError no timeout é distinto do TypeError de rede - classifique antes de mostrar a cópiaTelas são desmontadas quando os usuários navegam para longe no meio de uma requisição. Conecte AbortController à limpeza do useEffect:
import { useEffect, useState } from "react";
import { ActivityIndicator, Text, View } from "react-native";
export function PostDetail({ postId }: { postId: number }) {
const [post, setPost] = useState<{ title: string } | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const controller = new AbortController();
(async () => {
try {
const res = await fetch(`${API_BASE}/posts/${postId}`, {
signal: controller.signal,
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
setPost(await res.json());
} catch (e) {
if (e instanceof Error && e.name === "AbortError") return;
setError(e instanceof Error ? e.message : "Request failed");
}
})();
return () => controller.abort();
}, [postId]);
if (error) return <Text>{error}</Text>;
if (!post) return <ActivityIndicator />;
return <Text>{post.title}</Text>;
}AbortError no catch - cancelamento é esperado, não uma falha visível para o usuáriopostId cancela a requisição anterior automaticamente via reexecução do efeitoqueryKey + cancelamento embutido - veja TanStack Query em Dispositivos MóveisNunca faça um cast cego de res.json(). Valide uma vez na fronteira da rede:
import { z } from "zod";
const postSchema = z.object({
id: z.number(),
title: z.string(),
body: z.string(),
});
export type Post = z.infer<typeof postSchema>;
export async function getPostSafe(id: number): Promise<Post> {
const res = await fetchWithTimeout(`${API_BASE}/posts/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const raw: unknown = await res.json();
return postSchema.parse(raw);
}zod.parse lança ZodError - mapeie para "resposta inesperada do servidor" na UI, não um stack tracez.array(schema) - endpoints de lista desviam com mais frequência que endpoints de detalheUsuários móveis precisam de cópias diferentes para erros offline, de timeout e de servidor:
export type NetworkErrorKind = "offline" | "timeout" | "http" | "parse" | "unknown";
export function classifyFetchError(error: unknown, status?: number): NetworkErrorKind {
if (status && status >= 400) return "http";
if (error instanceof Error) {
if (error.name === "AbortError") return "timeout";
if (error instanceof TypeError) return "offline";
}
return "unknown";
}
export function userMessage(kind: NetworkErrorKind): string {
switch (kind) {
case "offline":
return "Sem conexão. Verifique o modo avião ou tente novamente quando estiver online.";
case "timeout":
return "Isso está demorando muito. Tente novamente com um sinal mais forte.";
case "http":
return "O servidor não pôde completar esta requisição.";
case "parse":
return "Recebemos uma resposta inesperada. Tente novamente mais tarde.";
default:
return "Algo deu errado. Por favor, tente novamente.";
}
}Mutações precisam de cabeçalhos explícitos. JSON.stringify em objetos simples - FormData é separado (uploads multipart):
export async function createPost(title: string, body: string) {
const res = await fetchWithTimeout(
`${API_BASE}/posts`,
{
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({ title, body, userId: 1 }),
},
12_000
);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return postSchema.parse(await res.json());
}Content-Type: application/json é obrigatório - omiti-lo quebra muitos parsers Rails/ExpressConstrua URLs de pesquisa com URLSearchParams - evita ?foo=undefined e erros de codificação:
export async function searchPosts(query: string, limit = 20) {
const params = new URLSearchParams({
q: query,
_limit: String(limit),
});
const res = await fetchWithTimeout(`${API_BASE}/posts?${params}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const raw: unknown = await res.json();
return z.array(postSchema).parse(raw);
}URLSearchParams lida com &, espaços e Unicodeparams.delete("q") quando a consulta estiver em brancocursor como um parâmetro de string; valide o formato com ZodUm módulo anexa tokens de portador, URL base e timeout padrão - recursos importam apiGet / apiPost:
// src/api/client.ts
import * as SecureStore from "expo-secure-store";
import { fetchWithTimeout } from "./fetchWithTimeout";
import { API_BASE } from "./config";
async function authHeaders(): Promise<HeadersInit> {
const token = await SecureStore.getItemAsync("access_token");
return {
Accept: "application/json",
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
};
}
export async function apiGet<T>(path: string, parse: (raw: unknown) => T): Promise<T> {
const res = await fetchWithTimeout(`${API_BASE}${path}`, {
headers: await authHeaders(),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return parse(await res.json());
}npx expo install expo-secure-storeEndpoints DELETE e alguns PATCH retornam 204 No Content. Chamar res.json() lança um erro:
export async function deletePost(id: number, headers: HeadersInit): Promise<void> {
const res = await fetchWithTimeout(`${API_BASE}/posts/${id}`, {
method: "DELETE",
headers,
});
if (res.status === 204) return;
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const text = await res.text();
if (!text) return;
// Algumas APIs retornam corpos JSON em DELETE - analise apenas quando presente
JSON.parse(text);
}Content-Type antes de analisar - páginas de erro text/html chegam como 200 de proxies mal configuradosres.text() e depois JSON.parse quando os corpos são opcionais - erros mais claros que res.json() em corpos vaziosAntes de adotar TanStack Query, entenda o padrão manual - depois substitua-o:
import { useCallback, useEffect, useState } from "react";
import { Pressable, Text, View } from "react-native";
import { API_BASE } from "../api/config";
import { fetchWithTimeout } from "../api/fetchWithTimeout";
import { postSchema, type Post } from "../api/posts";
export function PostScreen({ id }: { id: number }) {
const [data, setData] = useState<Post | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const load = useCallback(async (signal?: AbortSignal) => {
setLoading(true);
setError(null);
try {
const res = await fetchWithTimeout(`${API_BASE}/posts/${id}`, { signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
setData(postSchema.parse(await res.json()));
} catch (e) {
if (e instanceof Error && e.name === "AbortError") return;
setError(e instanceof Error ? e.message : "Failed");
} finally {
setLoading(false);
}
}, [id]);
useEffect(() => {
const controller = new AbortController();
void load(controller.signal);
return () => controller.abort();
}, [load]);
return (
<View style={{ padding: 16 }}>
{loading && <Text>Carregando…</Text>}
{error && (
<>
<Text>{error}</Text>
<Pressable onPress={() => load()}>
<Text>Tentar Novamente</Text>
</Pressable>
</>
)}
{data && <Text>{data.title}</Text>}
</View>
);
}load() - não Updates.reloadAsync() ou reinício do aplicativouseQuery quando precisar de cache, deduplicação e re-busca em segundo plano - TanStack Query em Dispositivos Móveisexport async function requestJson<T>(
path: string,
options: RequestInit & { timeoutMs?: number; parse: (raw: unknown) => T }
): Promise<T> {
const { timeoutMs = 10_000, parse, ...init } = options;
const res = await fetchWithTimeout(`${API_BASE}${path}`, init, timeoutMs);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return parse(await res.json());
}use do React 19 com Suspense (Opcional)Se sua equipe adotar Suspense para dados, mantenha o mesmo wrapper de timeout - apenas o local de consumo muda. A maioria dos aplicativos Expo ainda prefere TanStack Query para semântica de cache em dispositivos móveis.
res.ok. Correção: Cliente central lança erro em !res.ok.AbortController + padrão de 8–15 s.res.ok primeiro.useEffect retorna () => controller.abort().EXPO_PUBLIC_API_URL em produção sem guarda HTTPS - Texto claro vaza tokens. Correção: Afirme https:// no cliente de produção.Versõ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