React Typescript

Axios: HTTP-клієнт із розширеними можливостями

Опанування Axios як альтернативи fetch API: типізовані запити, interceptors, обробка помилок, скасування та кешування запитів у React із TypeScript

Axios: HTTP-клієнт із розширеними можливостями

🎯 Мета теми

  • Зрозуміти переваги Axios над вбудованим fetch API для виробничих проєктів.
  • Створити типізований Axios-інстанс із централізованою конфігурацією.
  • Опанувати перехоплювачі (interceptors) для автоматичної обробки токенів та помилок.
  • Інтегрувати Axios з React через кастомні хуки та обробку життєвого циклу запитів.

🔑 Ключові терміни

  • Axios Instance: налаштований екземпляр HTTP-клієнта з базовим URL, заголовками та таймаутами.
  • Interceptors: перехоплювачі запитів та відповідей для централізованої логіки автентифікації та обробки помилок.
  • Automatic JSON Transformation: автоматична серіалізація та десеріалізація JSON без ручного виклику .json().
  • Request Cancellation: скасування запитів при розмонтуванні компонента через AbortController.

Чому Axios, якщо є fetch?

Браузерний fetch API з'явився як сучасна заміна XMLHttpRequest і став стандартом для HTTP-запитів у JavaScript. Однак у реальних проєктах розробники часто стикаються з повторюваними завданнями: ручна серіалізація JSON, обробка помилок HTTP-статусів (fetch не вважає 404 або 500 помилкою!), додавання токенів автентифікації до кожного запиту, налаштування таймаутів.

Axios — це promise-based HTTP-клієнт, який вирішує ці проблеми «з коробки» та надає зручніший API для типових сценаріїв:

async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`https://api.example.com/users/${id}`, {
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${localStorage.getItem("token")}`,
    },
  });

  // fetch не вважає 404/500 помилкою — треба перевіряти вручну
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  }

  // JSON потрібно розпаковувати вручну
  const data: User = await response.json();
  return data;
}
Основна перевага Axios не в тому, що він «легший» за fetch (насправді він додає ~13 КБ до бандлу), а в тому, що він стандартизує роботу з HTTP у команді. Interceptors, централізована обробка помилок, автоматична трансформація даних та скасування запитів — все це можна реалізувати поверх fetch, але Axios надає готові, протестовані інструменти.

Встановлення та перший запит

npm install axios

Axios підтримує TypeScript «з коробки» — типи включені до основного пакету, додатковий @types/axios не потрібен.

Найпростіший запит виглядає ідентично fetch, але з автоматичною обробкою JSON:

import axios from "axios";
import { useEffect, useState } from "react";

interface User {
  id: number;
  name: string;
  email: string;
  phone: string;
}

function UserProfile() {
  const [user, setUser] = useState<User | null>(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    async function loadUser() {
      try {
        // response.data автоматично типізований як User
        const response = await axios.get<User>(
          "https://jsonplaceholder.typicode.com/users/1"
        );
        setUser(response.data);
      } catch (err) {
        if (axios.isAxiosError(err)) {
          // Axios надає type guard для перевірки типу помилки
          setError(err.response?.data?.message ?? err.message);
        } else {
          setError("Невідома помилка");
        }
      } finally {
        setIsLoading(false);
      }
    }

    loadUser();
  }, []);

  if (isLoading) return <p>Завантаження профілю...</p>;
  if (error) return <p className="error">Помилка: {error}</p>;
  if (!user) return <p>Користувача не знайдено</p>;

  return (
    <div className="user-profile">
      <h2>{user.name}</h2>
      <p>Email: {user.email}</p>
      <p>Телефон: {user.phone}</p>
    </div>
  );
}
Зверніть увагу на функцію axios.isAxiosError(err) — це type guard, що звужує тип unknown помилки до AxiosError<T>. Без цієї перевірки TypeScript не дозволить звертатися до err.response, оскільки загальний тип помилки у catch-блоці — unknown.

Створення типізованого Axios-інстансу

Викликати axios.get() напряму зручно для прототипів, але у виробничих застосунках створюють налаштований інстанс із базовим URL, заголовками, таймаутами та іншими параметрами, що повторюються у всіх запитах.

// src/api/axios.ts — єдина точка налаштування HTTP-клієнта
import axios, { AxiosInstance } from "axios";

const api: AxiosInstance = axios.create({
  baseURL: import.meta.env.VITE_API_URL ?? "https://jsonplaceholder.typicode.com",
  timeout: 10000, // 10 секунд — якщо запит триватиме довше, Axios кине помилку
  headers: {
    "Content-Type": "application/json",
  },
});

export default api;

Тепер усі запити використовують цей інстанс замість глобального axios:

// src/api/users.ts — модуль з API-викликами для користувачів
import api from "./axios";

interface User {
  id: number;
  name: string;
  email: string;
  phone: string;
}

interface CreateUserDTO {
  name: string;
  email: string;
  phone: string;
}

export const usersApi = {
  getAll: () => api.get<User[]>("/users"),
  getById: (id: number) => api.get<User>(`/users/${id}`),
  create: (data: CreateUserDTO) => api.post<User>("/users", data),
  update: (id: number, data: Partial<CreateUserDTO>) =>
    api.patch<User>(`/users/${id}`, data),
  delete: (id: number) => api.delete<void>(`/users/${id}`),
};
Організація API-викликів у окремі модулі (usersApi, postsApi, productsApi) робить кодову базу структурованішою та спрощує тестування — можна легко замокувати весь модуль однією інструкцією vi.mock("./api/users").

Interceptors: централізована обробка запитів та відповідей

Перехоплювачі (interceptors) — найпотужніша функція Axios. Вони дозволяють «перехопити» запит перед відправкою або відповідь перед поверненням до коду компонента, виконавши глобальну логіку: додавання токенів автентифікації, логування, трансформацію даних, автоматичне оновлення токенів при 401-помилці.

Request Interceptor: автоматичне додавання токена

import api from "./axios";

// Перехоплювач запитів — виконується ДО відправки запиту
api.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem("authToken");
    if (token) {
      // Додаємо токен до заголовків кожного запиту
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config; // Обов'язково повернути змінену конфігурацію
  },
  (error) => {
    // Обробка помилок ПЕРЕД відправкою (наприклад, немає мережі)
    return Promise.reject(error);
  }
);

Тепер кожен запит через цей інстанс автоматично отримує заголовок Authorization, якщо токен присутній у localStorage — не потрібно додавати його вручну у кожному виклику.

Response Interceptor: централізована обробка помилок

import { AxiosError } from "axios";
import api from "./axios";

interface ApiErrorResponse {
  message: string;
  statusCode: number;
  errors?: Record<string, string[]>; // Валідаційні помилки
}

// Перехоплювач відповідей — виконується ПІСЛЯ отримання відповіді
api.interceptors.response.use(
  (response) => {
    // Успішна відповідь (2xx) — просто повертаємо її далі
    return response;
  },
  (error: AxiosError<ApiErrorResponse>) => {
    // Помилка (4xx, 5xx або мережева помилка)
    if (error.response) {
      // Сервер відповів статусом поза діапазоном 2xx
      const { status, data } = error.response;

      switch (status) {
        case 401:
          // Несанкціонований доступ — скидаємо токен і перенаправляємо на логін
          localStorage.removeItem("authToken");
          window.location.href = "/login";
          break;

        case 403:
          // Доступ заборонено — показуємо сповіщення
          console.error("Доступ заборонено:", data.message);
          break;

        case 422:
          // Помилки валідації — обробляються окремо у компоненті форми
          console.warn("Валідаційні помилки:", data.errors);
          break;

        case 500:
        case 502:
        case 503:
          // Серверна помилка — логуємо для моніторингу
          console.error("Серверна помилка:", status, data.message);
          break;
      }
    } else if (error.request) {
      // Запит було відправлено, але відповідь не отримано (немає мережі)
      console.error("Немає з'єднання з сервером");
    } else {
      // Помилка налаштування запиту
      console.error("Помилка конфігурації:", error.message);
    }

    return Promise.reject(error);
  }
);
Будьте обережні з перенаправленнями у response interceptor (наприклад, window.location.href = ...). Вони спрацюють для кожного 401-запиту, що може створити багаторазове перенаправлення, якщо компонент робить кілька паралельних запитів. Для складніших сценаріїв використовуйте глобальний стан (Context API, Redux) та умовну логіку перенаправлення лише один раз.

Типізація помилок та обробка у компонентах

Axios надає generic-тип AxiosError<T>, де T — очікувана структура тіла помилки від сервера. Це дозволяє типізувати не лише успішні відповіді, а й помилки:

import { useState } from "react";
import axios, { AxiosError } from "axios";
import { usersApi } from "./api/users";

interface ApiErrorResponse {
  message: string;
  statusCode: number;
}

interface CreateUserFormData {
  name: string;
  email: string;
  phone: string;
}

function CreateUserForm() {
  const [formData, setFormData] = useState<CreateUserFormData>({
    name: "",
    email: "",
    phone: "",
  });
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [success, setSuccess] = useState(false);

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    setIsSubmitting(true);
    setError(null);
    setSuccess(false);

    try {
      await usersApi.create(formData);
      setSuccess(true);
      setFormData({ name: "", email: "", phone: "" }); // Очищуємо форму
    } catch (err) {
      // Type guard для Axios-помилок
      if (axios.isAxiosError<ApiErrorResponse>(err)) {
        // Тепер TypeScript знає про err.response.data.message
        setError(err.response?.data?.message ?? "Не вдалося створити користувача");
      } else if (err instanceof Error) {
        setError(err.message);
      } else {
        setError("Невідома помилка");
      }
    } finally {
      setIsSubmitting(false);
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <h2>Створити користувача</h2>

      {error && <div className="error-banner">{error}</div>}
      {success && <div className="success-banner">Користувача створено успішно!</div>}

      <label>
        Ім'я:
        <input
          type="text"
          value={formData.name}
          onChange={(e) => setFormData({ ...formData, name: e.target.value })}
          required
        />
      </label>

      <label>
        Email:
        <input
          type="email"
          value={formData.email}
          onChange={(e) => setFormData({ ...formData, email: e.target.value })}
          required
        />
      </label>

      <label>
        Телефон:
        <input
          type="tel"
          value={formData.phone}
          onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
          required
        />
      </label>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? "Створення..." : "Створити"}
      </button>
    </form>
  );
}
Type guard axios.isAxiosError<ApiErrorResponse>(err) не лише перевіряє, що помилка походить від Axios, а й звужує типerr.response.data до ApiErrorResponse. Це дозволяє TypeScript забезпечити автодоповнення та перевірку типів при зверненні до err.response.data.message.

Кастомний хук useAxios для повторного використання

Логіку завантаження даних через Axios зручно винести у кастомний хук, щоб уникнути дублювання коду у кожному компоненті:

// src/hooks/useAxios.ts
import { useEffect, useState } from "react";
import axios, { AxiosError, AxiosResponse } from "axios";

interface UseAxiosResult<T> {
  data: T | null;
  isLoading: boolean;
  error: string | null;
  refetch: () => void;
}

function useAxios<T>(
  fetcher: () => Promise<AxiosResponse<T>>,
  dependencies: unknown[] = []
): UseAxiosResult<T> {
  const [data, setData] = useState<T | null>(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [refetchTrigger, setRefetchTrigger] = useState(0);

  useEffect(() => {
    const controller = new AbortController();

    async function load() {
      try {
        setIsLoading(true);
        setError(null);

        const response = await fetcher();
        setData(response.data);
      } catch (err) {
        // Ігноруємо помилки скасованих запитів
        if (axios.isCancel(err)) return;

        if (axios.isAxiosError(err)) {
          setError(err.response?.data?.message ?? err.message);
        } else if (err instanceof Error) {
          setError(err.message);
        } else {
          setError("Невідома помилка");
        }
      } finally {
        setIsLoading(false);
      }
    }

    load();

    return () => {
      controller.abort(); // Скасовуємо запит при розмонтуванні
    };
  }, [...dependencies, refetchTrigger]);

  const refetch = () => setRefetchTrigger((prev) => prev + 1);

  return { data, isLoading, error, refetch };
}

export default useAxios;

Використання хука у компоненті:

import useAxios from "./hooks/useAxios";
import { usersApi } from "./api/users";

function UsersList() {
  const { data: users, isLoading, error, refetch } = useAxios(() => usersApi.getAll(), []);

  if (isLoading) return <p>Завантаження користувачів...</p>;
  if (error) return <p className="error">Помилка: {error}</p>;

  return (
    <div>
      <div className="header">
        <h2>Список користувачів</h2>
        <button onClick={refetch}>🔄 Оновити</button>
      </div>

      <ul className="users-list">
        {users?.map((user) => (
          <li key={user.id}>
            <strong>{user.name}</strong> — {user.email}
          </li>
        ))}
      </ul>
    </div>
  );
}
Функція refetch корисна для оновлення даних після виконання мутацій (створення, оновлення, видалення). Вона просто змінює значення refetchTrigger, що змушує useEffect виконатися заново та перезавантажити дані з сервера.

Скасування запитів при розмонтуванні компонента

Якщо компонент розмонтується до того, як завершиться HTTP-запит (користувач перейшов на іншу сторінку або закрив модальне вікно), спроба оновити стан призведе до попередження React: «Can't perform a React state update on an unmounted component». Axios підтримує скасування запитів через стандартний AbortController:

import { useEffect, useState } from "react";
import axios from "axios";
import { usersApi } from "./api/users";

function UserProfile({ userId }: { userId: number }) {
  const [user, setUser] = useState<User | null>(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    const controller = new AbortController();

    async function loadUser() {
      try {
        setIsLoading(true);
        // Передаємо signal у конфігурацію запиту
        const { data } = await usersApi.getById(userId, {
          signal: controller.signal,
        });
        setUser(data);
      } catch (err) {
        // axios.isCancel перевіряє, чи це помилка скасування
        if (axios.isCancel(err)) {
          console.log("Запит скасовано:", err.message);
          return; // Не оновлюємо стан для скасованих запитів
        }
        console.error("Помилка завантаження:", err);
      } finally {
        setIsLoading(false);
      }
    }

    loadUser();

    // Cleanup-функція викликається при розмонтуванні або зміні userId
    return () => {
      controller.abort();
    };
  }, [userId]);

  if (isLoading) return <p>Завантаження профілю...</p>;
  if (!user) return <p>Користувача не знайдено</p>;

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}
Axios автоматично інтегрується з AbortController — достатньо передати signal у конфігурацію запиту. При виклику controller.abort() Axios кине помилку типу CanceledError, яку можна перевірити через axios.isCancel(err).

Щоб API-функції підтримували скасування, їх потрібно розширити для прийому конфігурації:

// src/api/users.ts — оновлена версія з підтримкою AbortSignal
import api from "./axios";
import { AxiosRequestConfig } from "axios";

interface User {
  id: number;
  name: string;
  email: string;
  phone: string;
}

export const usersApi = {
  getAll: (config?: AxiosRequestConfig) => api.get<User[]>("/users", config),
  getById: (id: number, config?: AxiosRequestConfig) =>
    api.get<User>(`/users/${id}`, config),
  create: (data: CreateUserDTO, config?: AxiosRequestConfig) =>
    api.post<User>("/users", data, config),
  update: (id: number, data: Partial<CreateUserDTO>, config?: AxiosRequestConfig) =>
    api.patch<User>(`/users/${id}`, data, config),
  delete: (id: number, config?: AxiosRequestConfig) =>
    api.delete<void>(`/users/${id}`, config),
};

Axios vs fetch: порівняльна таблиця

ФункціяfetchAxios
JSON серіалізаціяРучна (JSON.stringify, response.json())Автоматична (data об'єкт)
Помилки HTTP-статусівresponse.ok потрібно перевіряти вручнуАвтоматично кидає помилку для 4xx/5xx
ТаймаутиНемає (потрібен AbortController + таймер)Вбудована опція timeout
InterceptorsНемає (треба обгортати fetch)Вбудовані request/response interceptors
Progress EventsПідтримка через ReadableStreamВбудована підтримка onUploadProgress
Скасування запитівAbortControllerAbortController + axios.isCancel()
Автоматичні трансформаціїНемаєТрансформація запиту/відповіді
Підтримка старих браузерівПотрібен поліфілПрацює у старих браузерах через XMLHttpRequest
Розмір бандлу0 КБ (вбудований)~13 КБ (мініфікований)
TypeScript типиВбудовані у lib.dom.d.tsВбудовані у пакет
Обирайте fetch, якщо:
  • Проєкт простий, без складної автентифікації та обробки помилок.
  • Критичний мінімальний розмір бандлу (кожен кілобайт важливий).
  • Використовується нативний Browser API без залежностей.
Обирайте Axios, якщо:
  • Потрібна централізована обробка токенів, помилок та трансформацій.
  • Працюєте з legacy API, що вимагає складних заголовків або форматів.
  • Потрібна підтримка старих браузерів без додаткових поліфілів.

Робота з файлами: завантаження та progress tracking

Axios спрощує завантаження файлів порівняно з fetch завдяки вбудованій підтримці FormData та відстеження прогресу:

import { useState } from "react";
import axios from "axios";

function FileUploadForm() {
  const [file, setFile] = useState<File | null>(null);
  const [uploadProgress, setUploadProgress] = useState(0);
  const [isUploading, setIsUploading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  async function handleUpload(e: React.FormEvent) {
    e.preventDefault();
    if (!file) return;

    const formData = new FormData();
    formData.append("file", file);
    formData.append("userId", "123"); // Додаткові поля

    try {
      setIsUploading(true);
      setError(null);

      await axios.post("https://api.example.com/upload", formData, {
        headers: {
          "Content-Type": "multipart/form-data",
        },
        onUploadProgress: (progressEvent) => {
          if (progressEvent.total) {
            const percentCompleted = Math.round(
              (progressEvent.loaded * 100) / progressEvent.total
            );
            setUploadProgress(percentCompleted);
          }
        },
      });

      alert("Файл завантажено успішно!");
    } catch (err) {
      if (axios.isAxiosError(err)) {
        setError(err.response?.data?.message ?? "Помилка завантаження");
      }
    } finally {
      setIsUploading(false);
      setUploadProgress(0);
    }
  }

  return (
    <form onSubmit={handleUpload}>
      <h2>Завантажити файл</h2>

      {error && <div className="error-banner">{error}</div>}

      <input
        type="file"
        onChange={(e) => setFile(e.target.files?.[0] ?? null)}
        disabled={isUploading}
      />

      {isUploading && (
        <div className="progress-bar">
          <div
            className="progress-fill"
            style={{ width: `${uploadProgress}%` }}
          >
            {uploadProgress}%
          </div>
        </div>
      )}

      <button type="submit" disabled={!file || isUploading}>
        {isUploading ? "Завантаження..." : "Завантажити"}
      </button>
    </form>
  );
}
Axios автоматично встановлює правильний Content-Type: multipart/form-data при передачі FormData, але явне зазначення заголовка у конфігурації дозволяє уникнути проблем з деякими серверними реалізаціями.

Axios із async/await та обробка паралельних запитів

Axios чудово поєднується з async/await та методами масивів для виконання множинних запитів:

Послідовне виконання (один після іншого)

async function loadUserWithPosts(userId: number) {
  try {
    // Спочатку завантажуємо користувача
    const { data: user } = await axios.get<User>(`/users/${userId}`);

    // Потім — його пости (залежить від userId)
    const { data: posts } = await axios.get<Post[]>(`/users/${userId}/posts`);

    return { user, posts };
  } catch (error) {
    console.error("Помилка завантаження даних:", error);
    throw error;
  }
}

Паралельне виконання (одночасно)

Якщо запити незалежні один від одного, виконуйте їх паралельно через Promise.all:

async function loadDashboardData() {
  try {
    // Всі три запити виконуються одночасно
    const [usersResponse, postsResponse, commentsResponse] = await Promise.all([
      axios.get<User[]>("/users"),
      axios.get<Post[]>("/posts"),
      axios.get<Comment[]>("/comments"),
    ]);

    return {
      users: usersResponse.data,
      posts: postsResponse.data,
      comments: commentsResponse.data,
    };
  } catch (error) {
    console.error("Помилка завантаження dashboard:", error);
    throw error;
  }
}
Пам'ятайте: Promise.allвідхилиться при першій помилці будь-якого запиту, і решта запитів (навіть якщо вони вже в процесі) не будуть оброблені. Якщо потрібна стійкість до часткових збоїв, використовуйте Promise.allSettled:
async function loadDashboardWithFallback() {
  const results = await Promise.allSettled([
    axios.get<User[]>("/users"),
    axios.get<Post[]>("/posts"),
    axios.get<Comment[]>("/comments"),
  ]);

  const users = results[0].status === "fulfilled" ? results[0].value.data : [];
  const posts = results[1].status === "fulfilled" ? results[1].value.data : [];
  const comments = results[2].status === "fulfilled" ? results[2].value.data : [];

  return { users, posts, comments };
}

Retry логіка та обробка тимчасових збоїв

У реальних застосунках мережеві збої — звична справа: нестабільне з'єднання, тимчасова недоступність сервера, таймаути. Axios не має вбудованої retry-логіки, але її легко реалізувати через interceptor або кастомну обгортку:

import axios, { AxiosError, AxiosInstance, AxiosRequestConfig } from "axios";

// Конфігурація retry-логіки
interface RetryConfig {
  retries: number; // Максимальна кількість повторів
  retryDelay: number; // Затримка між спробами (мс)
  retryCondition?: (error: AxiosError) => boolean; // Умова для повтору
}

// Функція затримки
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

// Обгортка Axios з retry-логікою
async function axiosWithRetry<T>(
  requestFn: () => Promise<T>,
  config: RetryConfig
): Promise<T> {
  const { retries, retryDelay, retryCondition } = config;
  let lastError: AxiosError;

  for (let attempt = 0; attempt <= retries; attempt++) {
    try {
      return await requestFn();
    } catch (error) {
      if (!axios.isAxiosError(error)) throw error;

      lastError = error;

      // Перевіряємо, чи потрібно повторювати запит
      const shouldRetry =
        attempt < retries &&
        (retryCondition ? retryCondition(error) : true);

      if (!shouldRetry) break;

      console.warn(`Спроба ${attempt + 1}/${retries} не вдалася. Повтор через ${retryDelay}мс...`);
      await delay(retryDelay);
    }
  }

  throw lastError!;
}

// Використання
async function fetchUsersWithRetry() {
  return axiosWithRetry(
    () => axios.get<User[]>("https://jsonplaceholder.typicode.com/users"),
    {
      retries: 3,
      retryDelay: 1000,
      retryCondition: (error) => {
        // Повторюємо лише при мережевих помилках або 5xx статусах
        return !error.response || (error.response.status >= 500 && error.response.status < 600);
      },
    }
  );
}
Бібліотека axios-retry надає готове рішення для retry-логіки з експоненційним backoff (затримка зростає після кожної невдалої спроби: 1с, 2с, 4с тощо). Встановлюється через npm install axios-retry та інтегрується за допомогою одного рядка: axiosRetry(api, { retries: 3 }).

Axios Defaults: глобальні налаштування

Окрім створення інстансів, Axios дозволяє змінювати глобальні налаштування, що застосовуються до всіх запитів у застосунку (навіть до тих, що викликаються напряму через axios.get()):

import axios from "axios";

// Глобальні налаштування для всіх запитів
axios.defaults.baseURL = "https://api.example.com";
axios.defaults.timeout = 15000;
axios.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest";
axios.defaults.headers.post["Content-Type"] = "application/json";

// Тепер усі запити використовують ці налаштування
async function fetchData() {
  const { data } = await axios.get("/users"); // URL: https://api.example.com/users
  return data;
}
Зміна axios.defaults впливає на весь застосунок, включаючи сторонні бібліотеки, які можуть використовувати Axios. У більшості випадків краще створювати окремі інстанси (axios.create()) для різних API або модулів проєкту.

Трансформація запитів та відповідей

Axios дозволяє глобально або локально трансформувати дані перед відправкою запиту чи після отримання відповіді. Це корисно для автоматичного перетворення дат, нормалізації полів або шифрування:

import axios from "axios";

const api = axios.create({
  baseURL: "https://api.example.com",

  // Трансформація даних ПЕРЕД відправкою на сервер
  transformRequest: [
    (data) => {
      // Автоматично конвертуємо об'єкти Date у ISO-рядки
      if (data && typeof data === "object") {
        for (const key in data) {
          if (data[key] instanceof Date) {
            data[key] = data[key].toISOString();
          }
        }
      }
      return JSON.stringify(data);
    },
  ],

  // Трансформація даних ПІСЛЯ отримання від сервера
  transformResponse: [
    (data) => {
      const parsed = JSON.parse(data);

      // Автоматично конвертуємо ISO-рядки назад у Date
      if (parsed && typeof parsed === "object") {
        for (const key in parsed) {
          if (typeof parsed[key] === "string" && /^\d{4}-\d{2}-\d{2}T/.test(parsed[key])) {
            parsed[key] = new Date(parsed[key]);
          }
        }
      }

      return parsed;
    },
  ],
});

// Тепер можна передавати Date безпосередньо
await api.post("/events", {
  title: "Зустріч команди",
  startDate: new Date("2026-10-20T10:00:00Z"),
});

// І отримувати Date у відповіді
const { data } = await api.get<{ startDate: Date }>("/events/1");
console.log(data.startDate instanceof Date); // true
Глобальні трансформації застосовуються до всіх запитів через цей інстанс. Якщо API має ендпоінти з різними форматами (JSON, XML, FormData), краще використовувати локальні трансформації через параметр transformRequest / transformResponse у конкретному запиті.

Практичний приклад: CRUD-додаток із Axios

Об'єднаємо всі вивчені концепції у повноцінному прикладі управління списком задач:

// src/api/axios.ts
import axios from "axios";

const api = axios.create({
  baseURL: "https://jsonplaceholder.typicode.com",
  timeout: 10000,
});

api.interceptors.request.use((config) => {
  const token = localStorage.getItem("authToken");
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});

api.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      localStorage.removeItem("authToken");
      window.location.href = "/login";
    }
    return Promise.reject(error);
  }
);

export default api;
// src/api/todos.ts
import api from "./axios";

export interface Todo {
  id: number;
  title: string;
  completed: boolean;
  userId: number;
}

export interface CreateTodoDTO {
  title: string;
  completed: boolean;
}

export const todosApi = {
  getAll: () => api.get<Todo[]>("/todos?_limit=10"),
  getById: (id: number) => api.get<Todo>(`/todos/${id}`),
  create: (data: CreateTodoDTO) => api.post<Todo>("/todos", data),
  update: (id: number, data: Partial<CreateTodoDTO>) =>
    api.patch<Todo>(`/todos/${id}`, data),
  delete: (id: number) => api.delete<void>(`/todos/${id}`),
};
// src/components/TodoApp.tsx
import { useEffect, useState } from "react";
import axios from "axios";
import { todosApi, Todo, CreateTodoDTO } from "../api/todos";

function TodoApp() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [newTodoTitle, setNewTodoTitle] = useState("");

  useEffect(() => {
    loadTodos();
  }, []);

  async function loadTodos() {
    try {
      setIsLoading(true);
      const { data } = await todosApi.getAll();
      setTodos(data);
    } catch (err) {
      if (axios.isAxiosError(err)) {
        setError(err.response?.data?.message ?? err.message);
      }
    } finally {
      setIsLoading(false);
    }
  }

  async function handleCreate(e: React.FormEvent) {
    e.preventDefault();
    if (!newTodoTitle.trim()) return;

    try {
      const newTodo: CreateTodoDTO = {
        title: newTodoTitle,
        completed: false,
      };

      const { data } = await todosApi.create(newTodo);
      setTodos([data, ...todos]); // Додаємо на початок списку
      setNewTodoTitle("");
    } catch (err) {
      if (axios.isAxiosError(err)) {
        alert("Помилка створення: " + err.message);
      }
    }
  }

  async function handleToggle(id: number, completed: boolean) {
    try {
      await todosApi.update(id, { completed: !completed });
      setTodos(
        todos.map((todo) =>
          todo.id === id ? { ...todo, completed: !completed } : todo
        )
      );
    } catch (err) {
      if (axios.isAxiosError(err)) {
        alert("Помилка оновлення: " + err.message);
      }
    }
  }

  async function handleDelete(id: number) {
    if (!confirm("Видалити цю задачу?")) return;

    try {
      await todosApi.delete(id);
      setTodos(todos.filter((todo) => todo.id !== id));
    } catch (err) {
      if (axios.isAxiosError(err)) {
        alert("Помилка видалення: " + err.message);
      }
    }
  }

  if (isLoading) return <p>Завантаження задач...</p>;
  if (error) return <p className="error">Помилка: {error}</p>;

  return (
    <div className="todo-app">
      <h1>Список задач</h1>

      <form onSubmit={handleCreate} className="todo-form">
        <input
          type="text"
          value={newTodoTitle}
          onChange={(e) => setNewTodoTitle(e.target.value)}
          placeholder="Нова задача..."
        />
        <button type="submit">Додати</button>
      </form>

      <ul className="todo-list">
        {todos.map((todo) => (
          <li key={todo.id} className={todo.completed ? "completed" : ""}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => handleToggle(todo.id, todo.completed)}
            />
            <span>{todo.title}</span>
            <button onClick={() => handleDelete(todo.id)}>Видалити</button>
          </li>
        ))}
      </ul>

      {todos.length === 0 && <p className="empty">Немає задач</p>}
    </div>
  );
}

export default TodoApp;

Axios у виробництві: best practices

✅ Створюйте окремі інстанси

Кожен API (бекенд, аналітика, зовнішні сервіси) має власний інстанс із налаштуваннями. Це спрощує тестування, міграції та підтримку.

✅ Централізуйте обробку помилок

Response interceptor — єдине місце для обробки 401 (перенаправлення на логін), 403 (доступ заборонено), 5xx (логування серверних помилок). Компоненти не повинні дублювати цю логіку.

✅ Типізуйте запити та відповіді

Використовуйте дженерики Axios: api.get<User[]>("/users"), api.post<Order>("/orders", data). TypeScript автоматично виведе типи для response.data.

✅ Скасовуйте запити при розмонтуванні

Завжди використовуйте AbortController у useEffect cleanup-функції, щоб уникнути оновлень стану розмонтованих компонентів.

⚠️ Не зберігайте токени у `localStorage`

Це вразливість до XSS-атак. Використовуйте httpOnly cookies або безпечні сховища токенів (наприклад, sessionStorage для short-lived токенів).

⚠️ Не ігноруйте таймаути

Завжди встановлюйте розумний timeout (5–15 секунд). Запити без таймауту можуть «зависнути» на хвилини при проблемах із мережею.

Питання для самоконтролю


Практичні завдання


Підсумок

✅ Опановані навички

  • Створення типізованих Axios-інстансів із централізованою конфігурацією
  • Використання interceptors для автентифікації та глобальної обробки помилок
  • Скасування запитів через AbortController при розмонтуванні компонентів
  • Типізація запитів, відповідей та помилок через дженерики Axios

🎯 Наступні кроки

Тепер, коли ви впевнено працюєте з Axios, час перейти до Redux Classic (тема 35) — класичного підходу до управління глобальним станом через екшени, редюсери та middleware для асинхронних запитів.
Copyright © 2026