Миграция React-приложения с Axios на Fetch
Мигрируйте React‑приложение с Axios на fetch через безопасную обертку, обработку ошибок, таймауты и готовый клиент без регрессий.
Миграция React-приложения с Axios на fetch — это малорисковое изменение при условии, что вы заменяете библиотеку за оберткой, а не переписываете места вызовов — и минное поле регрессий в противном случае. Оба клиента решают одну задачу по-разному: fetch — это нативный браузерный стандарт без зависимостей, предоставляющий сырые инструменты, тогда как Axios поставляет в комплекте удобства — базовый URL, автоматический JSON, автоматическое отклонение при ошибочных ответах, response.data, интерцепторы и таймауты, — которые вам придется воссоздавать самостоятельно. В этом руководстве сначала дается ответ на вопрос «стоит ли переходить», а затем предлагается последовательный, безопасный с точки зрения регрессий путь миграции для реального React-приложения, включая один полностью готовый к копированию клиент.
Ключевые выводы
- Самое опасное отличие:
fetch()не отклоняет промис при HTTP-ответах 4xx или 5xx — он успешно разрешается и возвращаетResponseсok: false, поэтому ответ 500 проходит через ваш блокtryкак будто это 200, если вы сами не проверяетеresponse.ok. - Мигрируйте обертку, а не места вызовов: если ваш новый
fetch-клиент предоставляет те же сигнатурыget/post/patch/deleteи возвращает те же промисы, React Query, ваши мутации и обработка ошибок никогда не узнают, что Axios исчез. - Используйте
AbortSignal.timeout(8000), чтобы датьfetchтаймаут запроса, который был встроен в Axios — он является Baseline-стандартом во всех браузерах с апреля 2024 года и прерывает запрос с ошибкойTimeoutError. fetchбольше не является только браузерным решением: это стабильный глобальный API в Node.js начиная с версии v21, что устраняет последнюю причину, по которой старый совет «оставьте Axios для Node» вообще был актуален.- Порядок важен: сначала замените все импорты на новый клиент, затем удалите
axiosи обновите lockfile — если сделать наоборот, сборка сломается в середине миграции.
Axios vs Fetch: вердикт
Выбирайте fetch, если используете лишь малую часть возможностей Axios и хотите избавиться от runtime-зависимости; оставайтесь на Axios, если активно используете его высокоуровневые функции. Axios предоставляет базовый URL, автоматический JSON, автоматическое отклонение при неуспешных ответах, response.data и интерцепторы из коробки; с fetch вы воссоздаете ровно то подмножество функций, которое реально использует ваше приложение — и ничего лишнего. Для большинства React SPA, которые уже централизуют запросы в одном API-модуле, это подмножество невелико.
Мотивы для миграции вполне конкретны:
- Размер бандла. Axios — это дополнительная runtime-зависимость объемом примерно 13–14 КБ в минифицированном и сжатом виде (в зависимости от версии), тогда как
fetch-обертка — это несколько сотен строк кода, которым вы владеете и который поддается tree-shaking. Точная цифра меняется с каждым релизом, поэтому измеряйте её для вашей установленной версии с помощью инструмента вроде Bundlephobia, а не полагайтесь на фиксированные числа. - Одна зависимость меньше / меньшая поверхность supply chain. Сокращение зависимостей — это также вопрос безопасности. В марте 2026 года злоумышленники опубликовали две бэкдорные версии Axios в npm через скомпрометированный аккаунт мейнтейнера, и каждый проект с плавающим диапазоном версий подтянул их при следующей установке. Разбор инцидента мейнтейнерами (GitHub issue #10636) и рекомендации по безопасности от Microsoft подтверждают: это была компрометация дистрибуции/аккаунта — две вредоносные опубликованные версии, а не уязвимость в исходном коде Axios — и плохие версии были удалены в течение нескольких часов; CISA также выпустила предупреждение. Вывод не в том, что «Axios небезопасен»; а в том, что одна зависимость меньше — это один аккаунт меньше, который может быть скомпрометирован от вашего имени.
- Вы используете лишь малую часть. Если ваш код выглядит как
axios.get(url).then(r => r.data), вы платите за библиотеку, чтобы сделать то, что делают восемь строк наfetch.
Когда оставить Axios
Оставьте Axios — миграция того не стоит, — если что-либо из следующего описывает ваше приложение:
| Оставьте Axios, если вы используете… | Почему fetch усложняет ситуацию |
|---|---|
| Активное использование интерцепторов запросов/ответов | У fetch нет системы интерцепторов; вам придется её воссоздать |
| UI с прогрессом загрузки файлов | Тела запросов fetch не предоставляют события прогресса загрузки; вам нужен XMLHttpRequest |
| Поддержку устаревших браузеров (например, IE11) | fetch там никогда не был доступен |
Широкий спектр специфичных для Axios настроек (paramsSerializer, трансформеры, адаптеры) | Воссоздание обойдется дороже, чем экономия от удаления зависимости |
Одна причина, которая больше не относится к этому списку — Node.js. fetch больше не является только браузерным решением: Node.js 18 (апрель 2022) поставил глобальный fetch, включенный по умолчанию, а Node.js 21 (октябрь 2023) перевел fetch и Web Streams в статус стабильных. Для более детального сравнения функций LogRocket предлагает разбор Axios vs Fetch, охватывающий прогресс загрузки, стриминг и отмену запросов.
Поведенческие различия, которые необходимо устранить
Discover how at OpenReplay.com.
Axios неявно делал шесть вещей, которые fetch делать не будет — и каждая из них является скрытым поведенческим изменением: код компилируется и работает, но ведет себя иначе. Усвойте эти различия перед написанием обертки.
fetchне отклоняет промис при 4xx/5xx. Это главная ловушка. Согласно справочнику MDN по Fetch API, промис разрешается, как только сервер отвечает заголовками — даже при ошибочном HTTP-статусе. Ответ 500 разрешается сresponse.ok === false. Axios автоматически отклоняет не-2xx ответы в.catch();fetch— нет. Вы должны сами проверитьresponse.okи выбросить исключение.- Нет автоматического JSON. Axios сериализует тела запросов и парсит ответы за вас. С
fetchвы вызываетеJSON.stringify()при отправке иawait response.json()при получении, а также вручную устанавливаетеContent-Type: application/json. - Нет
baseURL. Axios добавляет настроенный базовый URL.fetchпринимает полный URL или относительный к документу; вы сами объединяете пути. - Нет
response.data. Разобранное тело ответа находится заawait response.json(), а не в свойстве.data. - Нет интерцепторов. Нет встроенного механизма для глобального добавления токенов аутентификации или обработки 401.
- Нет опции таймаута. У Axios есть конфигурация
timeout; уfetch— нет. Вы передаетеAbortSignal.
До и после
Один и тот же вызов — на Axios и на клиенте, который вы создадите:
// Axios — авто-отклонение при ошибке, response.data, авто-JSON
const { data } = await axios.get(`/users/${id}`);
return data;
// fetch (сырой) — нужно проверять ok, парсить вручную, нет baseURL
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(res.statusText); // <-- без этого ответ 500 выглядит как успех
return res.json();
Стратегия: мигрируйте обертку, а не места вызовов
Не трогайте ваши компоненты, хуки или вызовы React Query. Замените библиотеку за существующим API-интерфейсом. Если ваше приложение уже маршрутизирует запросы через один модуль, предоставляющий get/post/patch/delete, вам нужно лишь сделать новую реализацию поведенчески совместимой — не совместимой с Axios. Сохраните идентичные сигнатуры, возвращайте те же промисы, выбрасывайте тот же тип ошибок — и всё нижестоящее останется нетронутым. Именно это делает миграцию скучной, а не рискованной.
Последовательность:
- Проведите аудит использования. Убедитесь, что вызовы проходят через одну обертку. Если нет, ваш первый PR — направить их через единый модуль; этот рефакторинг независим от
fetchи безопасен для самостоятельного деплоя. - Согласуйте контракт ошибок. Если ваш UI ветвится по
error.code === 'EXPIRED'или читаетerror.status, ваш новый клиент должен создавать ошибки с теми же полями. Несоответствие здесь — это скрытая поломка. - Сохраните форму промиса. React Query, SWR и ваши мутации важно лишь то, что вы возвращаете промис, который разрешается в данные или отклоняется с ошибкой — не то, какая библиотека его создала.
Полный fetch-клиент для копирования
Этот полный файл api-client.ts закрывает все пробелы из сравнения: объединение базового URL, параметры запроса, слияние заголовков, JSON-тело, проверка ok, отклоняющая ошибочные ответы, кастомный класс ошибок, инъекция токена аутентификации, централизованная обработка 401 и таймаут через AbortSignal.timeout(). Добавьте его в проект, направьте на него импорты — и места вызовов продолжат работать.
// lib/api-client.ts
const BASE_URL = import.meta.env.VITE_API_URL ?? "/api";
const DEFAULT_TIMEOUT = 8000;
export class ApiClientError extends Error {
status: number;
code?: string;
data?: unknown;
constructor(message: string, status: number, code?: string, data?: unknown) {
super(message);
this.name = "ApiClientError";
this.status = status;
this.code = code;
this.data = data;
}
}
type Query = Record<string, string | number | boolean | undefined>;
interface RequestOptions extends Omit<RequestInit, "body"> {
body?: unknown;
params?: Query;
timeout?: number;
}
function buildUrl(path: string, params?: Query): string {
const base = BASE_URL.endsWith("/") ? BASE_URL : `${BASE_URL}/`;
const url = new URL(path.replace(/^\//, ""), new URL(base, window.location.origin));
if (params) {
for (const [key, value] of Object.entries(params)) {
if (value !== undefined) url.searchParams.set(key, String(value));
}
}
return url.toString();
}
async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {
const { body, params, timeout = DEFAULT_TIMEOUT, headers, ...rest } = options;
const token = localStorage.getItem("token");
let res: Response;
try {
res = await fetch(buildUrl(path, params), {
...rest,
headers: {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
...headers,
},
body: body !== undefined ? JSON.stringify(body) : undefined,
signal: AbortSignal.timeout(timeout),
});
} catch (err) {
// Chromium может представить таймаут fetch как AbortError, а не TimeoutError.
if (err instanceof DOMException && (err.name === "TimeoutError" || err.name === "AbortError")) {
throw new ApiClientError("Request timed out", 0, "TIMEOUT");
}
throw new ApiClientError("Network error", 0, "NETWORK");
}
// Ловушка: fetch разрешается при 4xx/5xx. Отклоняем явно.
if (!res.ok) {
let payload: any;
try { payload = await res.json(); } catch { payload = undefined; }
if (res.status === 401) { // эквивалент интерцептора: централизованная обработка 401
localStorage.removeItem("token");
window.location.assign("/login");
}
throw new ApiClientError(
payload?.message ?? res.statusText,
res.status,
payload?.code,
payload,
);
}
if (res.status === 204) return undefined as T;
return (await res.json()) as T;
}
export const api = {
get: <T>(path: string, options?: RequestOptions) => request<T>(path, { ...options, method: "GET" }),
post: <T>(path: string, body?: unknown, options?: RequestOptions) => request<T>(path, { ...options, method: "POST", body }),
patch: <T>(path: string, body?: unknown, options?: RequestOptions) => request<T>(path, { ...options, method: "PATCH", body }),
put: <T>(path: string, body?: unknown, options?: RequestOptions) => request<T>(path, { ...options, method: "PUT", body }),
delete: <T>(path: string, options?: RequestOptions) => request<T>(path, { ...options, method: "DELETE" }),
};
Два момента, заслуживающих отдельного внимания. Таймаут использует AbortSignal.timeout(), который возвращает AbortSignal, автоматически прерывающий запрос по истечении заданного времени; при таймауте сигнал прерывается с TimeoutError DOMException. С апреля 2024 года эта функция работает во всех актуальных браузерах и на современных устройствах. Перехватывайте как TimeoutError, так и AbortError: в браузерах на базе Chromium таймаут fetch может проявляться как AbortError, а не TimeoutError, поэтому обрабатывайте оба имени в защитных целях. Короткое замыкание для 204 позволяет избежать вызова .json() на пустом теле ответа.
Воссоздание интерцепторов — и что оставить на XHR
Ваша обертка и есть ваш интерцептор. Всё, что делали интерцепторы Axios — добавление Bearer-токена, нормализация ошибок, редирект при 401 — находится в функции request() выше и применяется к каждому вызову по построению. Вам не нужна система плагинов; вам нужна одна точка прохождения, которая у вас уже есть. Добавление повтора при 429 или логирования запросов означает добавление нескольких строк в request(), а не регистрацию обработчика.
Одну вещь намеренно не стоит мигрировать: прогресс загрузки файлов. Тела запросов браузерного fetch не предоставляют события прогресса загрузки так, как это делает XMLHttpRequest, поэтому оставьте существующий XHR-поток для загрузки файлов. В гибридном подходе нет ничего зазорного: fetch для 95% JSON-вызовов, XHR для прогресс-бара при загрузке файлов.
Последовательность миграции
Выполняйте эти шаги по порядку. Именно с порядком люди чаще всего ошибаются.
- Сначала замените импорты. Направьте каждое место вызова на новый клиент (
import { api } from "@/lib/api-client"). Поскольку сигнатуры совпадают, это поиск с заменой, а не переписывание. - Удалите Axios, затем обновите lockfile. Удалите
axiosизpackage.jsonи запуститеnpm install(или эквивалент вашего пакетного менеджера) для обновления lockfile. Делайте это после замены импортов — в обратном порядке ваша сборка сломается в середине миграции с неразрешенными импортами. По состоянию на июнь 2026 года реестр npm указывает Axios версии 1.18.1; если вы фиксируете безопасную версию вместо удаления, проверьте реестр на наличие актуального релиза. - Проверяйте сборкой, а не чтением кода. Запустите тестовый набор, проверку типов TypeScript (
tsc --noEmit) и production-сборку. Миграция завершена, когда приложение собирается, проходит проверку типов и работает корректно — а не когда diff выглядит чисто.
Именно в последнем пункте скрываются молчаливые регрессии. Самая опасная регрессия при переходе с Axios на fetch — необработанный ошибочный ответ: запрос, вернувший 500, но из-за того, что fetch не выбрасывает исключение, прошедший как «успех» и оставивший пользователя смотреть на пустой или сломанный UI без отображения ошибки. Юнит-тесты мокируют счастливый путь и проходят; серверный лог показывает 500, поэтому бэкенд выглядит исправным. Разрыв между «сервер зафиксировал ошибку» и «пользователь ничего не увидел» — это именно то, что выявляет запись сессий с захватом сетевых запросов: она связывает перехваченный ошибочный ответ с реальным сломанным UI, который видел пользователь, обнаруживая регрессию, которую ваши тесты и серверные логи по отдельности упускают. Проверка !res.ok в клиенте выше — это то, что предотвращает данный класс ошибок с самого начала; убедиться, что она срабатывает в production, — значит подтвердить, что миграция действительно завершена.
fetch-клиент дает вам меньшее дерево зависимостей, полный контроль над поведением при ошибках и аутентификации, а также одну стороннюю учетную запись меньше в вашей цепочке поставок — без переписывания ни одного компонента, при условии сохранения идентичного API-интерфейса. Следующий конкретный шаг: если запросы вашего приложения ещё не проходят через единый модуль — направьте их туда, затем добавьте клиент выше и позвольте сборке, типам и тестам сообщить вам, когда вы закончили.
Часто задаваемые вопросы
Выбрасывает ли fetch ошибку при ответах 404 или 500?
Нет. fetch отклоняет промис только при сетевом сбое или незавершенном запросе; HTTP-ответ 404 или 500 разрешается успешно с объектом Response, у которого свойство ok равно false. Это означает, что серверная ошибка проходит через ваш блок try как успех, если вы явно не проверяете response.ok и не выбрасываете исключение самостоятельно. Axios ведет себя иначе и автоматически отклоняет любой не-2xx статус в путь catch.
Нужен ли мне ещё Axios для запросов в Node.js?
Нет. fetch является стабильным глобальным API в Node.js начиная с версии 21, выпущенной в октябре 2023 года, и был доступен как экспериментальный глобальный API, включенный по умолчанию, начиная с Node.js 18 в апреле 2022 года. Старый совет держать Axios специально для серверных запросов больше не актуален. Вы можете использовать один и тот же fetch-клиент как в браузерном, так и в Node-коде, хотя более старые версии Node до 18 по-прежнему требуют полифилла или библиотеки.
Как задать таймаут запроса с fetch?
Передайте AbortSignal.timeout(миллисекунды) в качестве опции signal, например signal: AbortSignal.timeout(8000). Это возвращает сигнал, который автоматически прерывается по истечении заданного времени, отклоняя fetch с TimeoutError DOMException. Функция является Baseline-стандартом во всех браузерах с апреля 2024 года. В браузерах на базе Chromium отклонение может проявляться как AbortError вместо TimeoutError, поэтому перехватывайте оба имени в защитных целях при разграничении таймаута и ручной отмены.
Сломает ли переход с Axios на fetch мою настройку React Query или SWR?
Нет, при условии, что ваш новый fetch-клиент возвращает те же промисы и предоставляет те же сигнатуры методов. React Query, SWR и ваши мутации важно лишь то, что вызов разрешается в данные или отклоняется с ошибкой — не то, какая библиотека это сделала. Сохраните идентичные сигнатуры get, post, patch и delete и выбрасывайте тот же тип ошибок с теми же полями, на которые ветвится ваш UI — и слой получения данных никогда не узнает, что Axios исчез.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k