Fetch vs axios
Interceptores, progreso de carga y compensaciones de tamaño de paquete para aplicaciones Expo SDK 57 que eligen entre el fetch integrado y axios.
Busca en todas las páginas de la documentación
Interceptores, progreso de carga y compensaciones de tamaño de paquete para aplicaciones Expo SDK 57 que eligen entre el fetch integrado y axios.
Tarjeta de referencia rápida - lista para copiar y pegar.
// src/api/http.ts - fetch wrapper con hooks tipo interceptor
import * as SecureStore from "expo-secure-store";
type RequestContext = { url: string; init: RequestInit };
type Interceptor = (ctx: RequestContext) => Promise<RequestContext> | RequestContext;
const requestInterceptors: Interceptor[] = [];
const responseHandlers: Array<(res: Response) => Response | Promise<Response>> = [];
export function onRequest(fn: Interceptor) {
requestInterceptors.push(fn);
}
export function onResponse(fn: (res: Response) => Response | Promise<Response>) {
responseHandlers.push(fn);
}
export async function http(input: string, init: RequestInit = {}) {
let ctx: RequestContext = { url: input, init: { ...init } };
for (const hook of requestInterceptors) {
ctx = await hook(ctx);
}
let res = await fetch(ctx.url, ctx.init);
for (const hook of responseHandlers) {
res = await hook(res);
}
return res;
}
// Adjunta bearer una vez
onRequest(async (ctx) => {
const token = await SecureStore.getItemAsync("access_token");
if (!token) return ctx;
return {
...ctx,
init: {
...ctx.init,
headers: { ...Object(ctx.init.headers), Authorization: `Bearer ${token}` },
},
};
});Cuándo usarlo:
fetch pero que necesitan un lugar para headers de autenticación y manejo de 401.// src/api/axiosClient.ts - cuando axios gana
import axios from "axios";
import * as SecureStore from "expo-secure-store";
const api = axios.create({
baseURL: process.env.EXPO_PUBLIC_API_URL,
timeout: 12_000,
headers: { Accept: "application/json" },
});
api.interceptors.request.use(async (config) => {
const token = await SecureStore.getItemAsync("access_token");
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
api.interceptors.response.use(
(res) => res,
async (error) => {
if (axios.isAxiosError(error) && error.response?.status === 401) {
// Activa flujo de renovación - ver Refresh Token Rotation
await SecureStore.deleteItemAsync("access_token");
}
return Promise.reject(error);
}
);
export { api };// src/screens/AvatarUploadScreen.tsx - progreso de carga vía XMLHttpRequest
import { useState } from "react";
import * as ImagePicker from "expo-image-picker";
import { ActivityIndicator, Pressable, Text, View } from "react-native";
function uploadWithProgress(
uri: string,
onProgress: (ratio: number) => void
): Promise<void> {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
const form = new FormData();
form.append("file", {
uri,
name: "avatar.jpg",
type: "image/jpeg",
} as unknown as Blob);
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) onProgress(e.loaded / e.total);
};
xhr.onload = () => (xhr.status >= 200 && xhr.status < 300 ? resolve() : reject());
xhr.onerror = () => reject(new TypeError("Network error"));
xhr.open("POST", `${process.env.EXPO_PUBLIC_API_URL}/upload`);
xhr.send(form);
});
}
export function AvatarUploadScreen() {
const [progress, setProgress] = useState(0);
const [busy, setBusy] = useState(false);
async function pickAndUpload() {
const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ["images"] });
if (result.canceled) return;
setBusy(true);
try {
await uploadWithProgress(result.assets[0].uri, setProgress);
} finally {
setBusy(false);
setProgress(0);
}
}
return (
<View style={{ padding: 16 }}>
<Pressable onPress={pickAndUpload} disabled={busy}>
<Text>Subir avatar</Text>
</Pressable>
{busy && (
<>
<ActivityIndicator />
<Text>{Math.round(progress * 100)}%</Text>
</>
)}
</View>
);
}npx expo install expo-image-picker expo-secure-store
npm install axiosLo que esto demuestra:
timeout integrado, baseURL e isAxiosError para ramas de error tipadas.XMLHttpRequest o multipart de expo-file-system - ni fetch ni axios exponen progreso en el polyfill de fetch de RN.| Necesidad | fetch | axios |
|---|---|---|
| Tamaño de paquete | ✅ Integrado | ⚠️ ~13–15 KB gzip minificado |
| Interceptores | DIY wrapper (~40 líneas) | ✅ Integrado |
| Timeout | AbortController | ✅ opción timeout |
| Progreso de carga | ❌ Usa XHR / FileSystem | ⚠️ axios de RN usa adapter - progreso vía XHR |
| Transformación JSON | Manual res.json() | ✅ Automático cuando Content-Type es JSON |
| Cancelar | AbortSignal | AbortController vía signal en config |
| Errores tipados | Clasificador personalizado | axios.isAxiosError |
# Mide antes de agregar axios a un paquete Hermes
npx expo export --platform ios
# Compara dist/_expo/static/js/ios/*.js antes/después de npm install axiosfollow-redirects y pipelines de transformación - significativo en Android de gama baja donde cada KB afecta TTI| Patrón | fetch wrapper | axios |
|---|---|---|
| Adjunta bearer | onRequest lee SecureStore | interceptors.request |
| Refresca en 401 | onResponse verifica estado, pone en cola refresco | interceptors.response |
| Header Request ID | Muta init.headers | config.headers["X-Request-Id"] |
| Reintento con backoff | No integrado - usa documento Retries | plugin axios-retry (dep extra) |
// Reintento idempotente pertenece fuera de cualquier cliente - utilidad compartida
import { fetchWithRetry } from "./retry";AxiosError.response?.data.axios + onUploadProgress en web - porta receta XHR arriba para paridad de RN.queryFn solo necesita un apiGet delgado.XMLHttpRequest o FileSystem.uploadAsync.res.json() + Zod es más ligero. Solución: Analiza en el edge per Networking Basics.timeout sin política de reintento - Un timeout simple en LTE parece servidores inestables. Solución: Empareja con backoff y claves de idempotencia.axios.create separado por plataforma o usa fetch en núcleo compartido.| Alternativa | Usa cuándo | No uses cuando |
|---|---|---|
| fetch + wrapper | Aplicaciones Expo por defecto, equipos pequeños | Necesitas plugins de reintento/carga batallados hoy |
| axios | Paquetes web/RN compartidos, APIs interceptor-pesadas | El tamaño de paquete ya está sobre presupuesto |
| Solo TanStack Query | Estado de servidor con caché | Aún necesitas un transporte - elige fetch o axios debajo |
carga expo-file-system | Archivos grandes, cargas amigables con fondo | Pequeños cuerpos JSON POST |
| tRPC / clientes generados | APIs tipadas end-to-end | REST de terceros público sin codegen |
Por defecto usa fetch con un pequeño wrapper api/client.ts. Agrega axios cuando interceptores, isAxiosError o convenciones de equipo justifiquen el costo de paquete.
No puedes en React Native hoy. Usa XMLHttpRequest (xhr.upload.onprogress) o uploadAsync de expo-file-system con callbacks de progreso.
Sí - pasa signal en la config de solicitud. TanStack Query suministra signal a queryFn; reenvíalo a axios para cancelación automática al desmontar.
Sí - queryFn: ({ signal }) => api.get('/posts', { signal }).then(r => r.data). Prefiere una instancia axios con interceptores en la raíz.
Aproximadamente 13–15 KB gzip para el cliente base - mide con expo export en tu aplicación. El código de wrapper a menudo es más pequeño que axios si solo necesitas headers de autenticación.
En 401, pausa solicitudes salientes, refresca una vez, reproduce cola o cierra sesión. Serializa refresco para evitar estampidas - ver Refresh Token Rotation.
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