Fetch vs axios
Interceptors, progresso de upload e trade-offs de tamanho de bundle para apps Expo SDK 57 escolhendo entre fetch embutido e axios.
Busque em todas as páginas da documentação
Interceptors, progresso de upload e trade-offs de tamanho de bundle para apps Expo SDK 57 escolhendo entre fetch embutido e axios.
Cartão de receita de referência rápida - pronto para copiar e colar.
// src/api/http.ts - wrapper fetch com hooks semelhantes a interceptors
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;
}
// Anexa o bearer uma 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}` },
},
};
});Quando usar isso:
fetch mas que precisam de um lugar para cabeçalhos de autenticação e tratamento de 401.// src/api/axiosClient.ts - quando axios vence
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) {
// Dispara o fluxo de refresh - veja Rotação de Token de Refresh
await SecureStore.deleteItemAsync("access_token");
}
return Promise.reject(error);
}
);
export { api };// src/screens/AvatarUploadScreen.tsx - progresso de upload via 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>Upload avatar</Text>
</Pressable>
{busy && (
<>
<ActivityIndicator />
<Text>{Math.round(progress * 100)}%</Text>
</>
)}
</View>
);
}npx expo install expo-image-picker expo-secure-store
npm install axiosO que isso demonstra:
timeout, baseURL e isAxiosError embutidos para ramos de erro tipados.XMLHttpRequest ou expo-file-system multipart - nem fetch nem axios expõem progresso no polyfill fetch do RN.| Necessidade | fetch | axios |
|---|---|---|
| Tamanho do Bundle | ✅ Embutido | ⚠️ ~13–15 KB gzip minificado |
| Interceptors | Wrapper DIY (~40 linhas) | ✅ Embutido |
| Timeout | AbortController | ✅ Opção timeout |
| Progresso de upload | ❌ Usar XHR / FileSystem | ⚠️ axios do RN usa adapter - progresso via XHR |
| Transformação JSON | res.json() manual | ✅ Automático quando Content-Type é JSON |
| Cancelar | AbortSignal | AbortController via signal na config |
| Erros tipados | Classificador customizado | axios.isAxiosError |
# Medir antes de adicionar axios a um bundle Hermes
npx expo export --platform ios
# Comparar dist/_expo/static/js/ios/*.js antes/depois de npm install axiosfollow-redirects e pipelines de transformação - significativo em Android de baixo desempenho onde cada KB afeta o TTI.| Padrão | Wrapper fetch | axios |
|---|---|---|
| Anexar bearer | onRequest lê SecureStore | interceptors.request |
| Refresh em 401 | onResponse verifica status, enfileira refresh | interceptors.response |
| Cabeçalho de ID de Requisição | Modifica init.headers | config.headers["X-Request-Id"] |
| Tentar novamente com backoff | Não embutido - use Documentação de Retentativas | Plugin axios-retry (dependência extra) |
// Retentativa idempotente pertence fora de qualquer cliente - utilidade compartilhada
import { fetchWithRetry } from "./retry";AxiosError.response?.data.axios + onUploadProgress na web - porte a receita XHR acima para paridade RN.queryFn só precisa de um apiGet fino.XMLHttpRequest ou FileSystem.uploadAsync.res.json() + Zod é mais leve. Correção: Analise na borda conforme Fundamentos de Rede.timeout do axios sem política de retentativa - Timeout único em LTE parece servidores instáveis. Correção: Combine com backoff e chaves de idempotência.axios.create separadas por plataforma ou use fetch no núcleo compartilhado.| Alternativa | Use Quando | Não Use Quando |
|---|---|---|
| fetch + wrapper | Apps Expo padrão, pequenas equipes | Você precisa de plugins de retentativa/upload testados em batalha hoje |
| axios | Pacotes web/RN compartilhados, APIs com muitos interceptors | O tamanho do bundle já estourou o orçamento |
| Apenas TanStack Query | Estado do servidor com cache | Você ainda precisa de um transporte - escolha fetch ou axios por baixo |
expo-file-system upload | Arquivos grandes, uploads em segundo plano | Pequenos corpos de POST JSON |
| tRPC / clientes gerados | APIs tipadas de ponta a ponta | REST de terceiros público sem codegen |
Use fetch por padrão com um pequeno wrapper api/client.ts. Adicione axios quando interceptors, isAxiosError, ou convenções de equipe justificarem o custo do bundle.
Você não pode no React Native hoje. Use XMLHttpRequest (xhr.upload.onprogress) ou uploadAsync do expo-file-system com callbacks de progresso.
Sim - passe signal na configuração da requisição. TanStack Query fornece signal para queryFn; passe-o para axios para cancelamento automático ao desmontar.
Sim - queryFn: ({ signal }) => api.get('/posts', { signal }).then(r => r.data). Prefira uma única instância axios com interceptors na raiz.
Aproximadamente 13–15 KB gzip para o cliente principal - meça com expo export no seu app. O código do wrapper é frequentemente menor que axios se você precisar apenas de cabeçalhos de autenticação.
Em 401, pause requisições de saída, atualize uma vez, reproduza a fila, ou deslogue. Serialize a atualização para evitar estampagens - veja Rotação de Token de Refresh.
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