Conceptos Básicos de Networking
10 ejemplos para tiempos de espera, cancelación y análisis en el borde móvil - la base que toda app Expo SDK 57 necesita antes de agregar TanStack Query o axios.
Busca en todas las páginas de la documentación
10 ejemplos para tiempos de espera, cancelación y análisis en el borde móvil - la base que toda app Expo SDK 57 necesita antes de agregar TanStack Query o axios.
React Native 0.86 incluye una implementación WHATWG fetch respaldada por pilas de networking nativas. Comienza con una app Expo en blanco:
npx create-expo-app@latest MyNetworkApp --template blank-typescript
cd MyNetworkApp
npx expo install zod @react-native-community/netinfoCrea un pequeño módulo API para que las pantallas nunca llamen a URLs sin procesar:
// src/api/config.ts
export const API_BASE =
process.env.EXPO_PUBLIC_API_URL ?? "https://jsonplaceholder.typicode.com";Herramientas: Estos ejemplos apuntan a Expo SDK 57 (
expo~57.0.4), React Native 0.86.0 y React 19.2.3. Prueba en un dispositivo físico sobre móvil - los simuladores ocultan el dolor del cambio de LTE.
El fetch móvil refleja las APIs web. Siempre verifica response.ok antes de analizar - HTTP 404 sigue resolviendo la promesa.
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 lanza en 4xx/5xx - debes inspeccionar res.ok o res.statusTypeError - a menudo indistinguibles de offline a primera vistaAPI_BASE - nunca disperses hosts de producción en carpetas de característicasRelacionado: Fetch vs axios - cuándo los interceptores justifican el costo del bundle
A diferencia de los navegadores, fetch de React Native no tiene opción timeout incorporada. Combina AbortController con 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 en timeout es distinto de la red TypeError - clasifica antes de mostrar copiaLas pantallas se desmontan cuando los usuarios se alejan durante una solicitud. Conecta AbortController a la limpieza de 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 en catch - la cancelación es esperada, no un fallo visible para el usuariopostId cancela automáticamente la solicitud anterior mediante la re-ejecución del efectoqueryKey + cancelación incorporada - ver TanStack Query on MobileNunca conviertas res.json() ciegamente. Valida una vez en el límite de red:
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);
}.parse lanza ZodError - mapea a "respuesta del servidor inesperada" en UI, no un rastreo de pilaz.array(schema) - los endpoints de lista se desvían más a menudo que los endpoints de detalleLos usuarios móviles necesitan copias diferentes para offline, timeout y errores del 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 "Sin conexión. Comprueba el modo avión o intenta de nuevo cuando estés en línea.";
case "timeout":
return "Esto está tomando demasiado tiempo. Intenta de nuevo con una señal más fuerte.";
case "http":
return "El servidor no pudo completar esta solicitud.";
case "parse":
return "Se recibió una respuesta inesperada. Intenta de nuevo más tarde.";
default:
return "Algo salió mal. Por favor, reintenta.";
}
}Las mutaciones necesitan encabezados explícitos. JSON.stringify en objetos simples - FormData es separado (cargas de varias partes):
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 es requerido - omitirlo rompe muchos analizadores de Rails/ExpressConstruye URLs de búsqueda con URLSearchParams - evita ?foo=undefined y errores de codificación:
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 maneja &, espacios y Unicodeparams.delete("q") cuando la consulta está vacíacursor como parámetro de cadena; valida la forma con ZodUn módulo adjunta tokens portadores, URL base y timeout predeterminado - las características importan 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-storeLos endpoints DELETE y algunos PATCH devuelven 204 No Content. Llamar a res.json() lanza:
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;
// Algunas APIs devuelven cuerpos JSON en DELETE - analiza solo cuando esté presente
JSON.parse(text);
}Content-Type antes de analizar - las páginas de error text/html llegan como 200 desde proxies mal configuradosres.text() luego JSON.parse cuando los cuerpos son opcionales - errores más claros que res.json() en vacíoAntes de adoptar TanStack Query, comprende el patrón manual - luego reemplázalo:
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>Cargando…</Text>}
{error && (
<>
<Text>{error}</Text>
<Pressable onPress={() => load()}>
<Text>Reintentar</Text>
</Pressable>
</>
)}
{data && <Text>{data.title}</Text>}
</View>
);
}load() - no Updates.reloadAsync() o reinicio de appuseQuery cuando necesites caché, deduplicación y actualización de fondo - TanStack Query on Mobileexport 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 con Suspense (Opcional)Si tu equipo adopta Suspense para datos, mantén el mismo envoltorio de timeout - solo el sitio de consumo cambia. La mayoría de apps Expo aún prefieren TanStack Query para la semántica de caché en móvil.
res.ok. Solución: El cliente central lanza en !res.ok.AbortController + 8-15 s predeterminado.res.ok primero.useEffect retorna () => controller.abort().EXPO_PUBLIC_API_URL en prod sin protección HTTPS - La fuga de texto claro filtra tokens. Solución: Afirma https:// en el cliente de producción.Versiones de Stack: 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