Уявіть, що ви будуєте панель керування для великого інтернет-магазину. Вам потрібно завантажувати товари, оновлювати ціни в реальному часі, обробляти помилки мережі та скасовувати зайві запити, коли користувач швидко перемикається між вкладками.
У "чистому" React ви швидко зіткнетеся з проблемою: логіка запитів починає змішуватися з UI-компонентами. Ваш useEffect розростається до 50 рядків, обробка помилок дублюється, а токени авторизації доводиться передавати вручну в кожен запит.
Сьогодні ми розберемо, як інтегрувати Axios у React 19 так, щоб ваш код був чистим, масштабованим та стійким до помилок. Ми не просто "зробимо запит", ми побудуємо професійний шар роботи з даними.
Хоча React не має "офіційного" способу робити запити, спільнота виробила певні стандарти. Чому ж більшість обирає Axios замість вбудованого fetch?
| Проблема Fetch в React | Рішення Axios |
|---|---|
Boilerplate: Потрібно постійно писати if (!res.ok) throw ... та await res.json() | Лаконічність: Автоматична валідація статусів та парсинг JSON зменшують код в useEffect. |
| Race Conditions: Складно скасовувати запити при розмонтуванні компонента | AbortController: Проста інтеграція з useEffect cleanup функцією. |
| Дублювання конфігу: Передача заголовків (Auth) у кожен виклик | Interceptors: Глобальне налаштування заголовків та обробки помилок в одному місці. |
Перш ніж будувати складні абстракції, розберемо "атомарний" рівень — як зробити запит правильно, враховуючи життєвий цикл React компонента.
Початківці часто роблять цю помилку — викликають запит прямо в тілі компонента або обробнику подій без врахування стану.
// ❌ НЕ РОБІТЬ ТАК
function BadComponent() {
const data = axios.get('/api/data'); // 😱 Запит буде виконуватись при кожному рендері!
return <div>{data.toString()}</div>;
}
Класичний підхід для отримання даних при завантаженні компонента.
Нам потрібно три стани для повного контролю UI: data (дані), loading (індикатор завантаження) та error (повідомлення про помилку).
Використовуємо useEffect з порожнім масивом залежностей [] для виконання коду один раз при монтуванні.
Критично важливо скасовувати запит, якщо компонент був видалений зі сторінки до завершення завантаження (наприклад, користувач швидко перейшов на іншу сторінку).
import { useState, useEffect } from 'react';
import axios from 'axios';
export function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// 1. Створюємо контролер для скасування
const controller = new AbortController();
const fetchUser = async () => {
try {
setIsLoading(true);
// 2. Передаємо сигнал в Axios
const response = await axios.get(`https://api.escuelajs.co/api/v1/users/${userId}`, {
signal: controller.signal
});
setUser(response.data);
setError(null);
} catch (err) {
// 3. Ігноруємо помилку, якщо це скасування запиту
if (axios.isCancel(err)) {
console.log('Request canceled', err.message);
} else {
setError(err.message);
}
} finally {
// 4. Оновлюємо стан завантаження, тільки якщо запит не був скасований
if (!controller.signal.aborted) {
setIsLoading(false);
}
}
};
fetchUser();
// 5. Функція очищення (Cleanup function)
return () => {
controller.abort();
};
}, [userId]); // Перезапускаємо ефект, якщо змінився userId
if (isLoading) return <div className="spinner">Завантаження...</div>;
if (error) return <div className="error">Помилка: {error}</div>;
if (!user) return null;
return (
<div className="card">
<img src={user.avatar} alt={user.name} />
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
Розбір коду:
AbortController: Це стандартний Web API. Якщо компонент демонтується (return з useEffect), ми викликаємо controller.abort(). Axios бачить це і перериває HTTP-з'єднання.axios.isCancel(err): Дозволяє відрізнити "поганий" запит (404, 500) від "скасованого" (користувач пішов зі сторінки), щоб не показувати помилку даремно.[userId]: Якщо ID зміниться, старий запит скасується, і почнеться новий.Писати axios.get прямо в компонентах — це шлях до дублювання коду. У професійних проєктах ми створюємо API Layer.
Створимо файл конфігурації, де живе "душа" нашої взаємодії з сервером.
import axios from 'axios';
// Створюємо єдиний екземпляр для всього додатку
export const apiClient = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'https://api.escuelajs.co/api/v1',
headers: {
'Content-Type': 'application/json',
},
timeout: 10000, // 10 секунд
});
// Interceptor для додавання токена (якщо є)
apiClient.interceptors.request.use((config) => {
const token = localStorage.getItem('auth_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Interceptor для глобальної обробки помилок
apiClient.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Логіка для розлогіну користувача
console.warn('Unauthorized! Redirecting to login...');
// window.location.href = '/login';
}
return Promise.reject(error);
}
);
Групуємо методи за бізнес-сутністю. Компоненти не повинні знати про URL-адреси.
import { apiClient } from './axios.client';
export const ProductsService = {
// Отримати всі продукти
getAll: async (params, signal) => {
const { data } = await apiClient.get('/products', { params, signal });
return data;
},
// Отримати один за ID
getById: async (id, signal) => {
const { data } = await apiClient.get(`/products/${id}`, { signal });
return data;
},
// Створити продукт
create: async (productData) => {
const { data } = await apiClient.post('/products', productData);
return data;
}
};
Тепер ми можемо створити власний хук, який інкапсулює логіку useEffect, станів та сервісів. Це зробить наші компоненти неймовірно чистими.
useFetch хукаЦей хук буде універсальним солдатом для GET-запитів.
import { useState, useEffect, useRef } from 'react';
import axios from 'axios';
export function useFetch(fetchFn, dependencies = []) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// Зберігаємо функцію у ref, щоб уникнути зайвих ререндерів
const fetchFnRef = useRef(fetchFn);
useEffect(() => {
fetchFnRef.current = fetchFn;
}, [fetchFn]);
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
setLoading(true);
setError(null);
// Передаємо signal у функцію запиту
const result = await fetchFnRef.current(controller.signal);
setData(result);
} catch (err) {
if (!axios.isCancel(err)) {
setError(err.response?.data?.message || err.message);
}
} finally {
if (!controller.signal.aborted) {
setLoading(false);
}
}
};
fetchData();
return () => {
controller.abort();
};
}, dependencies); // Залежить від переданих параметрів (наприклад, [categoryId])
return { data, loading, error };
}
Дивіться, наскільки чистішим став наш компонент!
import { useFetch } from '../hooks/useFetch';
import { ProductsService } from '../api/products.service';
export function ProductList({ categoryId }) {
// Хук бере на себе всю складність життєвого циклу
// Важливо: обгортаємо виклик сервісу в стрілочну функцію, щоб передати signal
const { data: products, loading, error } = useFetch(
(signal) => ProductsService.getAll({ categoryId }, signal),
[categoryId] // Перезавантажити, якщо змінилась категорія
);
if (loading) return <div className="skeleton">Loading...</div>;
if (error) return <div className="error">{error}</div>;
return (
<div className="grid grid-cols-3 gap-4">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
use)В React 19 з'явився новий хук use, який дозволяє читати проміси прямо в рендері. Це змінює правила гри, наближаючи клієнтський код до серверного.
Axios повертає Promise, тому ми можемо використовувати його з use. Але є критична умова: проміс повинен бути створений за межами рендеру або кешований, інакше ви отримаєте нескінченний цикл запитів.
import { use, Suspense } from 'react';
import { apiClient } from '../api/axios.client';
// 1. Створення ресурсу (простий кеш або запит на рівні модуля)
// В реальності використовуйте бібліотеки типу TanStack Query
const productsPromise = apiClient.get('/products').then(res => res.data);
function ProductList() {
// 2. Використання `use` для "розгортання" промісу
// Це призупинить (suspend) компонент, доки дані не завантажаться
const products = use(productsPromise);
return (
<ul>
{products.map(p => <li key={p.id}>{p.title}</li>)}
</ul>
);
}
export function Page() {
return (
// 3. Suspense обробляє стан завантаження
<Suspense fallback={<div>Loading products...</div>}>
<ProductList />
</Suspense>
);
}
use: Не створюйте проміси (axios.get(...)) прямо в тілі компонента перед передачею в use(). Це призведе до створення нового запиту при кожному рендері!Коли користувач натискає "Лайк", він не хоче чекати 200мс відповіді сервера. Ми оновлюємо UI миттєво, а запит шлемо у фоні.
import { useState } from 'react';
import { apiClient } from '../api/axios.client';
export function LikeButton({ productId, initialLikes }) {
const [likes, setLikes] = useState(initialLikes);
const [isLiked, setIsLiked] = useState(false);
const handleLike = async () => {
// 1. Оптимістичне оновлення (миттєве)
const previousLikes = likes;
const previousIsLiked = isLiked;
setLikes(prev => isLiked ? prev - 1 : prev + 1);
setIsLiked(!isLiked);
try {
// 2. Реальний запит у фоні
await apiClient.post(`/products/${productId}/like`);
} catch (error) {
// 3. Відкат (Rollback) при помилці
console.error('Like failed', error);
setLikes(previousLikes);
setIsLiked(previousIsLiked);
alert('Не вдалося поставити лайк :(');
}
};
return (
<button onClick={handleLike} className={isLiked ? 'liked' : ''}>
❤️ {likes}
</button>
);
}
Перевірте свій код на відповідність цим стандартам:
axios.create замість глобального імпорту?AbortController у useEffect?baseURL у змінні оточення (.env)?Спробуйте реалізувати компонент пошуку товарів, який:
import { useState, useEffect } from 'react';
import axios from 'axios';
// Простий хук debounce
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
export function SearchBar() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
const debouncedQuery = useDebounce(query, 500);
useEffect(() => {
if (!debouncedQuery) {
setResults([]);
return;
}
const controller = new AbortController();
const search = async () => {
setLoading(true);
try {
const { data } = await axios.get(`https://api.escuelajs.co/api/v1/products`, {
params: { title: debouncedQuery },
signal: controller.signal
});
setResults(data);
} catch (err) {
if (!axios.isCancel(err)) console.error(err);
} finally {
if (!controller.signal.aborted) setLoading(false);
}
};
search();
return () => controller.abort();
}, [debouncedQuery]);
return (
<div className="search-container">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Пошук..."
className="input"
/>
{loading && <p>Шукаємо...</p>}
<ul>
{results.map(p => <li key={p.id}>{p.title} - ${p.price}</li>)}
</ul>
</div>
);
}
React Hook Form: Глибоке Розуміння Архітектури та Оптимізації
TanStack Query: Майстерність Керування Станом Сервера
Ласкаво просимо до повного курсу з опанування TanStack Query (раніше відомого як React Query). Це не просто документація, це подорож від ручного фечингу даних до професійної архітектури синхронізації станів.