TanStack Query: Керування Станом Сервера
TanStack Query: Керування Станом Сервера
Ви щойно навчилися робити запити через Axios. Ваш код виглядає непогано, але з ростом додатку виникають нові проблеми:
- Кешування: Користувач перейшов на вкладку "Профіль", потім на "Головну", а потім знову на "Профіль". Чи потрібно знову робити запит, якщо дані не змінилися?
- Дедуплікація: Два компоненти на сторінці одночасно просять дані про поточного користувача. Ви відправите два запити?
- Актуальність: Як дізнатися, що дані на сервері змінилися, поки користувач дивиться на сторінку?
- Pagination/Infinite Scroll: Написання логіки "Завантажити ще" вручну — це біль.
Тут на сцену виходить TanStack Query (раніше React Query). Це не просто "бібліотека для запитів", це Async State Manager.
Концепція: Client State vs Server State
Щоб зрозуміти TanStack Query, треба розділити стан у вашій голові на два типи:
| Характеристика | Client State (UI) | Server State (Data) |
|---|---|---|
| Приклад | isModalOpen, inputValue, theme | Список товарів, дані користувача |
| Де живе | У пам'яті браузера | На віддаленому сервері |
| Контроль | Ми повністю контролюємо | Ми лише маємо "знімок" даних |
| Проблема | Синхронізація між компонентами | Застарівання (Staleness) |
| Інструмент | Context API, Zustand, Redux | TanStack Query |
loading, error, caching, який TanStack Query робить автоматично.Встановлення та Налаштування
1. Інсталяція
Ми будемо використовувати версію 5 (останню стабільну на момент React 19).
npm install @tanstack/react-query @tanstack/react-query-devtools
2. Підключення провайдера
Огорніть ваш додаток у QueryClientProvider. Це створить контекст для кешу.
import React from 'react';
import ReactDOM from 'react-dom/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import App from './App';
// 1. Створюємо клієнт
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// Глобальні налаштування
staleTime: 1000 * 60, // Дані вважаються "свіжими" 1 хвилину
},
},
});
ReactDOM.createRoot(document.getElementById('root')).render(
<QueryClientProvider client={queryClient}>
<App />
{/* DevTools — це мастхев для дебагу! */}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
Отримання даних: useQuery
Забудьте про useEffect + useState + isLoading. Хук useQuery робить все це за вас.
Анатомія хука
const { data, isLoading, isError, error } = useQuery({
queryKey: ['unique-key'], // Унікальний ідентифікатор в кеші
queryFn: fetchFunction, // Функція, що повертає Promise
});
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
// Функція запиту (краще виносити в окремий файл api)
const fetchProducts = async () => {
const { data } = await axios.get('https://api.escuelajs.co/api/v1/products');
return data;
};
function ProductsList() {
const { data, isLoading, isError, error } = useQuery({
queryKey: ['products'],
queryFn: fetchProducts,
});
if (isLoading) return <div>Завантаження...</div>;
if (isError) return <div>Помилка: {error.message}</div>;
return (
<ul>
{data.map(product => (
<li key={product.id}>{product.title}</li>
))}
</ul>
);
}
Магія Query Keys
queryKey — це масив, який працює як масив залежностей в useEffect. Якщо значення в масиві змінюється, Query автоматично робить новий запит.
// ✅ Отримати список
useQuery({ queryKey: ['todos'], ... })
// ✅ Отримати конкретний елемент (фільтрація)
useQuery({ queryKey: ['todos', { status: 'done' }], ... })
// ✅ Отримати по ID
useQuery({
queryKey: ['todos', todoId],
queryFn: () => fetchTodoById(todoId)
})
Найважливіші концепції: StaleTime vs GcTime
Це те, де плутаються 90% новачків.
staleTime (Час свіжості)
Питання: "Як довго ці дані вважаються актуальними, перш ніж я спробую оновити їх?"
- За замовчуванням: 0 ms.
- Це означає, що Query за замовчуванням вважає дані застарілими миттєво. При кожному перефокусуванні вікна або повторному монтуванні компонента буде відбуватися фоновий запит.
- Якщо ви встановите
staleTime: 5000(5 секунд), то протягом 5 секунд запити не будуть відправлятися, навіть якщо ви викличете їх знову.
gcTime (Garbage Collection Time, раніше cacheTime)
Питання: "Як довго тримати невикористовувані дані в пам'яті?"
- За замовчуванням: 5 хвилин.
- Якщо компонент демонтується, дані залишаються в "холодному сховищі". Якщо користувач повернеться протягом 5 хвилин — дані миттєво з'являться з кешу, поки йде оновлення. Якщо ні — вони видаляються для звільнення пам'яті.
Мутації: useMutation
Для зміни даних (POST, PUT, DELETE) використовується useMutation. На відміну від useQuery, мутації не запускаються автоматично — ви викликаєте їх вручну.
Патерн: "Invalidation" (Оновлення даних)
Це "Святий Грааль" TanStack Query. Коли ми щось змінюємо на сервері, наш локальний кеш стає неактуальним. Ми повинні сказати Query: "Познач ці дані як старі і завантаж нові".
import { useMutation, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';
function CreateProduct() {
const queryClient = useQueryClient(); // Доступ до кешу
const mutation = useMutation({
mutationFn: (newProduct) => {
return axios.post('https://api.escuelajs.co/api/v1/products', newProduct);
},
// Виконується при успішному запиті
onSuccess: () => {
// 💥 БУМ! Ми кажемо: дані за ключем ['products'] застаріли.
// Query автоматично зробить refetch там, де цей ключ використовується.
queryClient.invalidateQueries({ queryKey: ['products'] });
alert('Продукт створено!');
},
});
return (
<button
onClick={() => mutation.mutate({ title: 'New Item', price: 100 })}
disabled={mutation.isPending}
>
{mutation.isPending ? 'Створення...' : 'Створити Продукт'}
</button>
);
}
ProductsList сам оновить дані. Це гарантує "Single Source of Truth".Advanced: Оптимістичні оновлення (Optimistic Updates)
Коли ви ставите "лайк", ви не хочете чекати. Ви хочете бачити червоне сердечко миттєво.
Алгоритм:
- Оновити UI до запиту.
- Зробити запит.
- Якщо помилка — відкотити зміни назад.
- Якщо успіх — (опціонально) завантажити "справжні" дані.
import { useMutation, useQueryClient } from '@tanstack/react-query';
function LikeButton({ postId }) {
const queryClient = useQueryClient();
const { mutate } = useMutation({
mutationFn: likePostApi,
// 1. Виконується ДО запиту
onMutate: async (newLike) => {
// Зупиняємо будь-які фонові оновлення цього поста, щоб вони не перезаписали наш оптимізм
await queryClient.cancelQueries({ queryKey: ['post', postId] });
// Зберігаємо попередній стан (snapshot) для можливого відкату
const previousPost = queryClient.getQueryData(['post', postId]);
// Оновлюємо кеш вручну "оптимістично"
queryClient.setQueryData(['post', postId], (old) => ({
...old,
likes: old.likes + 1,
isLiked: true,
}));
// Повертаємо snapshot у контекст
return { previousPost };
},
// 2. Якщо сталася помилка
onError: (err, newLike, context) => {
// Відкочуємося до попереднього стану
queryClient.setQueryData(['post', postId], context.previousPost);
},
// 3. Завжди після завершення (успіх чи помилка)
onSettled: () => {
// Синхронізуємося з сервером для певності
queryClient.invalidateQueries({ queryKey: ['post', postId] });
},
});
return <button onClick={() => mutate()}>❤️ Like</button>;
}
React 19: Suspense Integration
В React 19 Suspense стає стандартом для обробки завантаження. TanStack Query v5 має спеціальний хук для цього — useSuspenseQuery.
Він гарантує, що data завжди визначена (не undefined), а стан завантаження обробляється найближчим <Suspense> boundary.
import { Suspense } from 'react';
import { useSuspenseQuery } from '@tanstack/react-query';
function UserProfile({ userId }) {
// Тут немає isLoading! Компонент "зависне", якщо даних немає.
const { data } = useSuspenseQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
return <h1>{data.name}</h1>;
}
export function App() {
return (
<Suspense fallback={<div className="skeleton">Loading Profile...</div>}>
<UserProfile userId={1} />
</Suspense>
);
}
Практичні поради (Best Practices)
Колокація ключів
queryKey поруч із функцією запиту. Створіть об'єкт productKeys (factory pattern), щоб уникнути помилок при написанні рядків:
const productKeys = { all: ['products'], detail: (id) => ['products', id] }DevTools
ReactQueryDevtools у розробці. Це єдиний спосіб побачити, що насправді відбувається у вашому кеші (що свіже, що застаріле, що завантажується).Custom Hooks
useQuery прямо в UI-компонентах. Створіть кастомний хук useProducts, useCreateProduct. Це дозволить змінювати логіку (наприклад, staleTime) в одному місці.Підсумок
TanStack Query — це не просто заміна useEffect. Це зміна парадигми.
- Server State != Client State.
- Query Keys — це ваші залежності.
- Invalidation — це спосіб синхронізації після змін.
- StaleTime контролює частоту запитів.
Використовуючи цей інструмент, ви видаляєте сотні рядків коду, пов'язаного зі станами завантаження, і отримуєте додаток, який відчувається "миттєвим" для користувача завдяки розумному кешуванню.
Axios та React: Професійна Архітектура Запитів
Уявіть, що ви будуєте панель керування для великого інтернет-магазину. Вам потрібно завантажувати товари, оновлювати ціни в реальному часі, обробляти помилки мережі та скасовувати зайві запити, коли користувач швидко перемикається між вкладками.
Atomic Design