React

TanStack Query: Керування Станом Сервера

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

TanStack Query: Керування Станом Сервера

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

  • Кешування: Користувач перейшов на вкладку "Профіль", потім на "Головну", а потім знову на "Профіль". Чи потрібно знову робити запит, якщо дані не змінилися?
  • Дедуплікація: Два компоненти на сторінці одночасно просять дані про поточного користувача. Ви відправите два запити?
  • Актуальність: Як дізнатися, що дані на сервері змінилися, поки користувач дивиться на сторінку?
  • Pagination/Infinite Scroll: Написання логіки "Завантажити ще" вручну — це біль.

Тут на сцену виходить TanStack Query (раніше React Query). Це не просто "бібліотека для запитів", це Async State Manager.

Аналогія: Якщо Axios — це кур'єр, який бігає за посилкою, то TanStack Query — це завгосп, який каже: "Стій, у нас ця посилка вже лежить на складі з минулого разу, не біжи дарма".

Концепція: Client State vs Server State

Щоб зрозуміти TanStack Query, треба розділити стан у вашій голові на два типи:

ХарактеристикаClient State (UI)Server State (Data)
ПрикладisModalOpen, inputValue, themeСписок товарів, дані користувача
Де живеУ пам'яті браузераНа віддаленому сервері
КонтрольМи повністю контролюємоМи лише маємо "знімок" даних
ПроблемаСинхронізація між компонентамиЗастарівання (Staleness)
ІнструментContext API, Zustand, ReduxTanStack Query
Головна помилка: Намагатися запихнути Server State у Redux або Zustand. Це змушує вас писати тонни boilerplate-коду для обробки loading, error, caching, який TanStack Query робить автоматично.

Встановлення та Налаштування

1. Інсталяція

Ми будемо використовувати версію 5 (останню стабільну на момент React 19).

npm install @tanstack/react-query @tanstack/react-query-devtools

2. Підключення провайдера

Огорніть ваш додаток у QueryClientProvider. Це створить контекст для кешу.

main.jsx
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
});

Магія Query Keys

queryKey — це масив, який працює як масив залежностей в useEffect. Якщо значення в масиві змінюється, Query автоматично робить новий запит.

// ✅ Отримати список
useQuery({ queryKey: ['todos'], ... })

// ✅ Отримати конкретний елемент (фільтрація)
useQuery({ queryKey: ['todos', { status: 'done' }], ... })

// ✅ Отримати по ID
useQuery({ 
  queryKey: ['todos', todoId], 
  queryFn: () => fetchTodoById(todoId) 
})
Loading diagram...

graph TD AComponent Renders --> B{Check Cache for Key} B -- Found & Fresh --> CReturn Cached Data B -- Found & Stale --> DReturn Cached Data D --> ERefetch in Background B -- Not Found --> FFetch Data

style E stroke:#f59e0b,stroke-width:2px
style C stroke:#10b981,stroke-width:2px

Найважливіші концепції: 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: "Познач ці дані як старі і завантаж нові".

CreateProduct.jsx
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)

Коли ви ставите "лайк", ви не хочете чекати. Ви хочете бачити червоне сердечко миттєво.

Алгоритм:

  1. Оновити UI до запиту.
  2. Зробити запит.
  3. Якщо помилка — відкотити зміни назад.
  4. Якщо успіх — (опціонально) завантажити "справжні" дані.
LikeButton.jsx
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.

SuspenseComponent.jsx
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. Це зміна парадигми.

  1. Server State != Client State.
  2. Query Keys — це ваші залежності.
  3. Invalidation — це спосіб синхронізації після змін.
  4. StaleTime контролює частоту запитів.

Використовуючи цей інструмент, ви видаляєте сотні рядків коду, пов'язаного зі станами завантаження, і отримуєте додаток, який відчувається "миттєвим" для користувача завдяки розумному кешуванню.