Axios: HTTP-клієнт із розширеними можливостями
Axios: HTTP-клієнт із розширеними можливостями
🎯 Мета теми
- Зрозуміти переваги Axios над вбудованим
fetchAPI для виробничих проєктів. - Створити типізований 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;
}
import axios from "axios";
async function fetchUser(id: number): Promise<User> {
// Axios автоматично:
// 1. Додає Content-Type: application/json
// 2. Парсить JSON із response.data
// 3. Кидає помилку при статусах 4xx/5xx
const { data } = await axios.get<User>(`https://api.example.com/users/${id}`);
return data;
}
Встановлення та перший запит
npm install axios
pnpm add axios
yarn add 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}`),
};
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);
}
);
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>
);
}
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>
);
}
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: порівняльна таблиця
| Функція | fetch | Axios |
|---|---|---|
| JSON серіалізація | Ручна (JSON.stringify, response.json()) | Автоматична (data об'єкт) |
| Помилки HTTP-статусів | response.ok потрібно перевіряти вручну | Автоматично кидає помилку для 4xx/5xx |
| Таймаути | Немає (потрібен AbortController + таймер) | Вбудована опція timeout |
| Interceptors | Немає (треба обгортати fetch) | Вбудовані request/response interceptors |
| Progress Events | Підтримка через ReadableStream | Вбудована підтримка onUploadProgress |
| Скасування запитів | AbortController | AbortController + axios.isCancel() |
| Автоматичні трансформації | Немає | Трансформація запиту/відповіді |
| Підтримка старих браузерів | Потрібен поліфіл | Працює у старих браузерах через XMLHttpRequest |
| Розмір бандлу | 0 КБ (вбудований) | ~13 КБ (мініфікований) |
| TypeScript типи | Вбудовані у lib.dom.d.ts | Вбудовані у пакет |
fetch, якщо:- Проєкт простий, без складної автентифікації та обробки помилок.
- Критичний мінімальний розмір бандлу (кожен кілобайт важливий).
- Використовується нативний Browser API без залежностей.
- Потрібна централізована обробка токенів, помилок та трансформацій.
- Працюєте з 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>
);
}
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
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.get<User[]>("/users"), api.post<Order>("/orders", data). TypeScript автоматично виведе типи для response.data.✅ Скасовуйте запити при розмонтуванні
AbortController у useEffect cleanup-функції, щоб уникнути оновлень стану розмонтованих компонентів.⚠️ Не зберігайте токени у `localStorage`
httpOnly cookies або безпечні сховища токенів (наприклад, sessionStorage для short-lived токенів).⚠️ Не ігноруйте таймаути
timeout (5–15 секунд). Запити без таймауту можуть «зависнути» на хвилини при проблемах із мережею.Питання для самоконтролю
fetch повертає rejected promise лише при мережевих помилках (немає інтернету, CORS, DNS-проблеми). HTTP-статуси 4xx та 5xx вважаються «успішною відповіддю від сервера», тому response.ok буде false, але promise — fulfilled. Розробник повинен вручну перевіряти response.ok або response.status та кидати помилку:
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
Axios, навпаки, автоматично кидає помилку для всіх статусів поза діапазоном 2xx, що спрощує обробку у try/catch.
Якщо компонент розмонтується до завершення запиту, спроба викликати setState (наприклад, setData(response.data)) призведе до попередження React:
Warning: Can't perform a React state update on an unmounted component.
Це не критична помилка, але вказує на потенційну витрату пам'яті (промісу все ще очікує завершення) та логічну помилку (оновлення стану неіснуючого компонента). Скасування через controller.abort() гарантує, що запит буде перерваний, а cleanup-логіка — виконана коректно.
axios.isAxiosError(err) — це type guard (захисник типу), який перевіряє, чи об'єкт err є екземпляром AxiosError. TypeScript не може автоматично визначити тип помилки у catch-блоці (він завжди unknown), тому без type guard неможливо звертатися до специфічних для Axios полів:
try {
await api.get("/users");
} catch (err) {
// err має тип unknown — TypeScript не дозволяє err.response
if (axios.isAxiosError(err)) {
// Тепер err має тип AxiosError, і можна безпечно звертатися до полів
console.log(err.response?.status);
console.log(err.response?.data);
}
}
Без цієї перевірки доведеться використовувати any або as AxiosError, що знижує типобезпеку.
Дженерик <User> типізує поле response.data. Без нього TypeScript виводить тип any для data, що позбавляє автодоповнення та перевірки типів:
// Без дженерика — data має тип any
const response = await axios.get("/users/1");
console.log(response.data.name); // Немає автодоповнення, помилки не виявляться
// З дженериком — data має тип User
const response = await axios.get<User>("/users/1");
console.log(response.data.name); // ✅ Автодоповнення працює
console.log(response.data.invalidField); // ❌ Помилка компіляції
Це не впливає на runtime-поведінку (Axios все одно повертає JSON), але надає типобезпеку на етапі розробки.
Практичні завдання
Створіть Axios-інстанс для JSONPlaceholder API з наступними вимогами:
- Базовий URL:
https://jsonplaceholder.typicode.com - Таймаут: 5 секунд
- Request interceptor: додає заголовок
X-Custom-Header: MyApp - Response interceptor: логує статус та URL кожного запиту у консоль
- Реалізуйте функцію
fetchWithRetry, яка повторює запит максимум 2 рази при мережевих помилках
Критерії успіху:
- Запит
api.get("/users/999")(неіснуючий користувач) логує статус 404 у консоль - Запит до неіснуючого домену повторюється 2 рази перед остаточною помилкою
- Всі запити містять заголовок
X-Custom-Header
import axios, { AxiosError, AxiosInstance } from "axios";
// Крок 1: Створюємо інстанс із базовими налаштуваннями
const api: AxiosInstance = axios.create({
baseURL: "https://jsonplaceholder.typicode.com",
timeout: 5000,
});
// Крок 2: Request interceptor — додає кастомний заголовок
api.interceptors.request.use((config) => {
config.headers["X-Custom-Header"] = "MyApp";
return config;
});
// Крок 3: Response interceptor — логування
api.interceptors.response.use(
(response) => {
console.log(`✅ ${response.status} ${response.config.url}`);
return response;
},
(error: AxiosError) => {
if (error.response) {
console.log(`❌ ${error.response.status} ${error.config?.url}`);
} else {
console.log(`❌ Мережева помилка: ${error.message}`);
}
return Promise.reject(error);
}
);
// Крок 4: Функція з retry-логікою
async function fetchWithRetry<T>(
requestFn: () => Promise<T>,
maxRetries: number = 2
): Promise<T> {
let lastError: Error;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await requestFn();
} catch (error) {
lastError = error as Error;
// Повторюємо лише при мережевих помилках (не при 404/500)
if (axios.isAxiosError(error) && error.response) {
throw error; // Серверна помилка — не повторюємо
}
if (attempt < maxRetries) {
console.warn(`⚠️ Спроба ${attempt + 1} не вдалася. Повторюємо...`);
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
}
throw lastError!;
}
// Тестування
async function testApi() {
try {
// Тест 1: Успішний запит
const { data: user } = await fetchWithRetry(() => api.get("/users/1"));
console.log("Користувач:", user.name);
// Тест 2: 404 (не повторюється)
await fetchWithRetry(() => api.get("/users/999"));
} catch (err) {
console.error("Остаточна помилка:", err);
}
}
testApi();
Покрокове пояснення:
axios.create()створює ізольований інстанс із власними налаштуваннями, не впливаючи на глобальнийaxios.- Request interceptor перехоплює кожен запит перед відправкою та модифікує
config.headers. - Response interceptor отримує як успішні відповіді (
response), так і помилки (error), логуючи їх у консоль. fetchWithRetryвикористовує цикл для повторних спроб, але не повторює запити при серверних помилках (4xx/5xx), оскільки повторний запит дасть той самий результат.
Створіть повністю типізований API-клієнт для роботи з постами блогу:
Моделі даних:
interface Post {
id: number;
title: string;
body: string;
userId: number;
}
interface CreatePostDTO {
title: string;
body: string;
userId: number;
}
Вимоги:
- Базовий URL:
https://jsonplaceholder.typicode.com - Методи:
postsApi.getAll()— отримати всі постиpostsApi.getById(id)— отримати пост за IDpostsApi.create(data)— створити новий постpostsApi.update(id, data)— оновити пост (PATCH)postsApi.delete(id)— видалити пост
- Всі методи мають бути строго типізовані
Бонус: Створіть React-компонент PostsManager, що використовує цей API для відображення списку постів із можливістю створення та видалення.
// src/api/posts.ts
import axios, { AxiosResponse } from "axios";
const api = axios.create({
baseURL: "https://jsonplaceholder.typicode.com",
timeout: 10000,
});
interface Post {
id: number;
title: string;
body: string;
userId: number;
}
interface CreatePostDTO {
title: string;
body: string;
userId: number;
}
export const postsApi = {
getAll: (): Promise<AxiosResponse<Post[]>> =>
api.get<Post[]>("/posts?_limit=10"),
getById: (id: number): Promise<AxiosResponse<Post>> =>
api.get<Post>(`/posts/${id}`),
create: (data: CreatePostDTO): Promise<AxiosResponse<Post>> =>
api.post<Post>("/posts", data),
update: (
id: number,
data: Partial<CreatePostDTO>
): Promise<AxiosResponse<Post>> =>
api.patch<Post>(`/posts/${id}`, data),
delete: (id: number): Promise<AxiosResponse<void>> =>
api.delete<void>(`/posts/${id}`),
};
// src/components/PostsManager.tsx
import { useEffect, useState } from "react";
import axios from "axios";
import { postsApi, Post, CreatePostDTO } from "../api/posts";
function PostsManager() {
const [posts, setPosts] = useState<Post[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [newPost, setNewPost] = useState<CreatePostDTO>({
title: "",
body: "",
userId: 1,
});
useEffect(() => {
loadPosts();
}, []);
async function loadPosts() {
try {
const { data } = await postsApi.getAll();
setPosts(data);
} catch (err) {
if (axios.isAxiosError(err)) {
alert("Помилка завантаження: " + err.message);
}
} finally {
setIsLoading(false);
}
}
async function handleCreate(e: React.FormEvent) {
e.preventDefault();
if (!newPost.title.trim() || !newPost.body.trim()) return;
try {
const { data } = await postsApi.create(newPost);
setPosts([data, ...posts]);
setNewPost({ title: "", body: "", userId: 1 });
} catch (err) {
if (axios.isAxiosError(err)) {
alert("Помилка створення: " + err.message);
}
}
}
async function handleDelete(id: number) {
if (!confirm("Видалити цей пост?")) return;
try {
await postsApi.delete(id);
setPosts(posts.filter((p) => p.id !== id));
} catch (err) {
if (axios.isAxiosError(err)) {
alert("Помилка видалення: " + err.message);
}
}
}
if (isLoading) return <p>Завантаження постів...</p>;
return (
<div className="posts-manager">
<h1>Блог</h1>
<form onSubmit={handleCreate} className="create-form">
<input
type="text"
placeholder="Заголовок поста"
value={newPost.title}
onChange={(e) => setNewPost({ ...newPost, title: e.target.value })}
/>
<textarea
placeholder="Вміст поста"
value={newPost.body}
onChange={(e) => setNewPost({ ...newPost, body: e.target.value })}
/>
<button type="submit">Опублікувати</button>
</form>
<div className="posts-list">
{posts.map((post) => (
<article key={post.id} className="post-card">
<h3>{post.title}</h3>
<p>{post.body}</p>
<button onClick={() => handleDelete(post.id)}>Видалити</button>
</article>
))}
</div>
</div>
);
}
export default PostsManager;
Ключові моменти типізації:
- Експліцитні типи повернення (
Promise<AxiosResponse<Post>>) гарантують, що TypeScript точно знає структуру відповіді. Partial<CreatePostDTO>у методіupdateдозволяє передавати лише окремі поля для оновлення.- Type guard
axios.isAxiosError(err)у компоненті забезпечує безпечний доступ доerr.messageтаerr.response.
Підсумок
✅ Опановані навички
- Створення типізованих Axios-інстансів із централізованою конфігурацією
- Використання interceptors для автентифікації та глобальної обробки помилок
- Скасування запитів через
AbortControllerпри розмонтуванні компонентів - Типізація запитів, відповідей та помилок через дженерики Axios
🎯 Наступні кроки
Data Router API: альтернативний синтаксис через createBrowserRouter
Повний довідник альтернативного об'єктного синтаксису React Router v7 через createBrowserRouter: від базових маршрутів до складних вкладених структур, динамічних параметрів, loaders, actions та обробки помилок.
Redux: класичний підхід до глобального стану
Опанування класичного Redux від основ до інтеграції з React: actions, reducers, store, middleware, react-redux хуки та порівняння з Context API