TanStack Query для React: кешування, оновлення даних та зміни
Замініть шаблон fetch з useEffect на TanStack Query: ключі запитів, staleTime, gcTime, мутації та ситуації, коли бібліотека є зайвою.
Як кешується, оновлюється та поновлюється асинхронний стан сервера — і коли варто додавати цю бібліотеку.
Багато додатків на React починаються з однакової структури завантаження даних: useEffect, fetch та кілька флагів useState для відображення інформації про очікування та помилки. Ця схема підходить для початкової версії. Саме тут також з’являються дубльовані запити, застарілі екрани та шаблонний код, скопійований з інших проектів.
У наступних розділах розглядаються ці проблеми та показується, як TanStack Query їх вирішує. Єдиними передумовами є хуки React та базовий TypeScript. У прикладах використовується DummyJSON — публічний API, який не вимагає ключа API.
1. Базова схема
Список продуктів, написаний за допомогою звичайних хуків, виглядає ось так.
import { useEffect, useState } from "react";
type Product = {
id: number;
title: string;
price: number;
};
function ProductList() {
const [products, setProducts] = useState<Product[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
setIsLoading(true);
fetch("https://dummyjson.com/products?limit=10")
.then((res) => {
if (!res.ok) throw new Error("Request failed");
return res.json();
})
.then((data: { products: Product[] }) => {
if (!cancelled) setProducts(data.products);
})
.catch((err: Error) => {
if (!cancelled) setError(err.message);
})
.finally(() => {
if (!cancelled) setIsLoading(false);
});
return () => {
cancelled = true;
};
}, []);
if (isLoading) return <p>Loading…</p>;
if (error) return <p>{error}</p>;
return (
<ul>
{products.map((product) => (
<li key={product.id}>
{product.title} - ${product.price}
</li>
))}
</ul>
);
}
Цей опис вже є ретельним: у ньому використовується флаг cancelled, тож повільна відповідь не може оновити стан після виведення з роботи. Багато реальних кодових баз пропускають цей захист.
2. Що не обробляє цей код
Сам запит fetch є нормальним. Проблеми полягають у тому, що відбувається навколо нього.
- Відсутність кешу. Якщо покинути маршрут та повернутися, мережевий запит виконується знову, навіть якщо дані не змінилися.
- Відсутність усунення дублікатів запитів. Три компоненти, яким потрібен один і той самий список продуктів, надсилають три ідентичні запити.
- Відсутність повторних спроб. Одна втрачена з’єднаність призводить до появи помилки в інтерфейсі, навіть якщо друга спроба могла б вдатися.
- Відсутність перевірки даних. Вкладка, яка залишена відкритою протягом години, продовжує відображати дані старі на годину, поки щось інше не спричинить їх завантаження.
cancelled допомагає під час видалення компоненту; проте він не повністю вирішує проблему перекриття оброблюваних запитів.Для кожної з цих проблем існує відоме рішення. Реалізація цих рішень вручну означає необхідність створення власного шару кешування.
3. Ідея бібліотеки
Корисно розділяти стани на два типи.
Клієнтський стан належить інтерфейсу користувача: чи відкрито модальне вікно, яке поле форми активне, яка тема обрана. Він змінюється лише тоді, коли це змінює ваш код. Для цього підходить useState.
Стан сервера є позиченим. Він зберігається у магазині, яким ви не керуєте. Інші користувачі можуть його змінити, а копія у браузері є лише знімком на момент часу. Зберігання цього знімка лише у useState створює ілюзію того, що тимчасовий перегляд є авторитетним.
Дані, отримані в позику, потребують спеціального магазину: місця для копії, позначки про термін дії та правила, яке визначає, коли знову отримати дані.
Холодильник є доречною аналогією. Молоко залишається вдома, щоб не ходити до магазину за кожною чашкою кави, проте воно псується, тому потрібно перевіряти дату та поповнювати запаси перед тим, як воно зіпсується. TanStack Query виконує цю функцію для відповідей API.
4. Що таке TanStack Query
TanStack Query керує асинхронним віддаленим станом у браузерних додатках. Проєкт розроблений під ліцензією MIT, його можна використовувати безкоштовно, і він поширений у проєктах на React.
Посібники, написані багато років тому, досі згадують React Query. Ця назва залишалась до версії 3. У версії 4 проект було перейменовано на TanStack Query після того, як з’явилися адаптери для Vue, Svelte, Solid та Angular. У React ви все ще інсталюєте @tanstack/react-query; поточною є основна версія 5.
Це не заміна для fetch чи axios. Функція запиту залишається у вашому розпорядженні. Бібліотека визначає таймінг, зберігання, свіжість даних та обробку помилок навколо цієї функції.
5. Налаштування
Для початку достатньо двох кроків.
npm install @tanstack/react-query
Потім обгорніть всю структуру один раз біля кореня:
// main.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import App from "./App";
const queryClient = new QueryClient();
export default function Root() {
return (
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
);
}
QueryClient — це екземпляр кешу. QueryClientProvider робить його доступним для кожного компонента-нащадка.
6. Той самий компонент, переписаний
import { useQuery } from "@tanstack/react-query";
type Product = {
id: number;
title: string;
price: number;
};
async function fetchProducts(): Promise<Product[]> {
const res = await fetch("https://dummyjson.com/products?limit=10");
if (!res.ok) throw new Error("Request failed");
const data: { products: Product[] } = await res.json();
return data.products;
}
function ProductList() {
const { data, isPending, isError, error } = useQuery({
queryKey: ["products"],
queryFn: fetchProducts,
});
if (isPending) return <p>Loading…</p>;
if (isError) return <p>{error.message}</p>;
return (
<ul>
{data.map((product) => (
<li key={product.id}>
{product.title} - ${product.price}
</li>
))}
</ul>
);
}
Приблизно сорок рядків скорочуються до близько п’ятнадцяти. Перемінна data вказується як Product[] без додаткових приміток, оскільки її тип випливає з функції fetchProducts. Після обробки умов isPending та isError TypeScript вважає, що data вже визначена, тому не потрібен опційний ланцюговий доступ до map та жодних заперечень наявності значення.
7. Що дає коротша версія
У порівнянні з проблемами, описаними у розділі 2, стандартні значення вже охоплюють поширені сценарії:
- Функція Remount негайно відображає збережений контент та перевіряє його у фоновому режимі.
- Ідентичні запити, що виконуються одночасно, об’єднуються в один запит.
- Невдалі запити автоматично повторюються (за замовчуванням три спроби із затримкою).
- Старі дані оновлюються при зосередженні уваги на вікні, підключенні мережі та повторному завантаженні.
Жодна з цих функцій не була налаштована у переписаному компоненті — це стандартна поведінка бібліотеки.
8. Три речі, які варто зрозуміти
Більшість початкових плутанин пов’язані з наступними трьома аспектами.
Ключ запиту
queryKey — це адреса кешу. Два компоненти, які використовують ["products"], діляться одним записом та одним мережевим запитом.
Основне правило: кожен параметр, від якого залежить функція запиту, має бути присутнім у ключі.
function ProductList({ category }: { category: string }) {
const { data } = useQuery({
queryKey: ["products", category],
queryFn: () => fetchProductsByCategory(category),
});
// …
}
Якщо з ключа пропущено category, зміна категорії все одно може призвести до відображення кешованого списку попередньої категорії. Ця помилка дуже поширена серед новачків.
staleTime та gcTime
Ці назви звучать схоже, але означають різні речі.
staleTime визначає проміжок часу свіжості даних. У межах цього проміжку бібліотека уникає мережевих операцій. За замовчуванням значення становить 0, тож результат відразу вважається застарілим: інтерфейс все ще може відображати збережені дані, проте будь-який запуск викликає фонове оновлення. Підвищуйте це значення, якщо дані рідко змінюються:
useQuery({
queryKey: ["products"],
queryFn: fetchProducts,
staleTime: 5 * 60 * 1000, // fresh for five minutes
});
gcTime — це час, протягом якого невикористані дані залишаються в пам’яті після того, як останній користувач вийшов. За замовчуванням цей проміжок становить п’ять хвилин. Коли він закінчується, запис видаляється, і наступний доступ до даних відбувається з нуля.
Коротко кажучи: staleTime керує процесом повторного завантаження; gcTime — процесом видалення.
Коли відбувається повторне завантаження
За замовчуванням застарілий запит оновлюється при монтуванні компонента, коли вікно знову отримує фокус та при повторному підключенні до мережі. Кожну з цих функцій можна вимкнути як на клієнті, так і для окремого запиту:
useQuery({
queryKey: ["products"],
queryFn: fetchProducts,
refetchOnWindowFocus: false,
});
Функція оновлення при отриманні фокусу дивує користувачів уперше, коли вони її бачать. Зазвичай саме завдяки її увімкненню довгострокова вкладка залишається актуальною.
9. Зміна даних за допомогою useMutation
useQuery використовується для читання даних. useMutation — для їх запису.
import { useMutation, useQueryClient } from "@tanstack/react-query";
type NewProduct = {
title: string;
price: number;
};
function AddProductButton() {
const queryClient = useQueryClient();
const { mutate, isPending } = useMutation({
mutationFn: async (product: NewProduct) => {
const res = await fetch("https://dummyjson.com/products/add", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(product),
});
if (!res.ok) throw new Error("Could not add product");
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["products"] });
},
});
return (
<button
onClick={() => mutate({ title: "New product", price: 25 })}
disabled={isPending}
>
{isPending ? "Saving…" : "Add product"}
</button>
);
}
Важливою є строка invalidateQueries. Цей виклик позначає кожен елемент кешу під префіксом ["products"] як застарілий, тож спостерігачі, що були монтовані, негайно запитують свіжі дані. Локальні масиви не коригуються вручну, і сторінці не потрібне повне перезавантаження.
10. Інструменти розробника
npm install @tanstack/react-query-devtools
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
У процесі розробки панель відображає кожен ключ запиту, статус, дані та час останнього отримання інформації. Спостереження за зміною статусу записів з «свіжих» на «старі» під час навігації допомагає краще зрозуміти принципи роботи кешу, ніж просте читання інструкцій. Цей пакет автоматично видаляється з версій для продакшну.
Поширені помилки
- Виключення змінних із ключа запиту. Якщо функція запиту читає певне значення, воно обов’язково має бути включене до ключа.
- Використання
useQueryвсерединіuseEffect. Цей хук вже виконується під час рендерингу; немає потреби щось додатково «запускати». - Використання бібліотеки для керування лише станом на клієнті. Поля форм та флаги модалних вікон мають знаходитися у
useState. - Встановлення
staleTime: Infinityскрізь. Це вимикає перевірку даних, що позбавляє систему більшості переваг.
data у локальний стан. Тоді ви підтримуєте дві копії, а та, що відображається, припиняє відстежувати кеш.12. Коли це може не знадобитися
Якщо додаток використовує лише один кінцевий пункт на одному екрані, використання провайдерів та хуків може бути зайвим порівняно з реальною необхідністю.
Якщо фреймворк вже надає шар даних — серверні компоненти Next.js або маршрутизатор із лоадерами — частина роботи вже виконана. TanStack Query все ще може допомогти з інтерактивним отриманням даних на клієнті, але це не є обов’язковим.
Для стану, який ніколи не залишає браузер, оберіть інструмент іншого типу.
13. Куди рухатися далі
Повсякденна робота охоплюється вищезазначеними концепціями. Більш глибокі теми розглядаються в інших джерелах:
- Офіційна документація — посібники та інтерактивні демонстрації
- Сторінковані та нескінченні списки — використовуйте
useInfiniteQuery, щоб під час прокрутки завантажувати додаткові сторінки - Запити, які чекають на інші операції — блокуйте подальші запити за допомогою параметра
enabled, поки не завершиться попередня операція - Оптимістичний інтерфейс користувача — відображайте бажаний результат ще до отримання відповіді від операції змін
Пам’ятайте одне: віддалені дані мають зберігатися у кеші, а не у стані окремих компонентів. З огляду на цю модель решта API стає зрозумілою.