TanStack Query: Майстерність Керування Станом Сервера

Основи Запитів та Магія Ключів

Тепер, коли ми маємо налаштований інструмент, час навчитися ним користуватися. У цій главі ми розберемо useQuery — основний хук, який ви будете використовувати у 90% випадків.

Основи Запитів та Магія Ключів

Тепер, коли ми маємо налаштований інструмент, час навчитися ним користуватися. У цій главі ми розберемо useQuery — основний хук, який ви будете використовувати у 90% випадків.

Ми також заглибимося в Query Keys — концепцію, від якої залежить коректність роботи вашого кешу. Якщо ви зрозумієте ключі неправильно, ви отримаєте баги з кешуванням.

Анатомія useQuery

У версії 5 useQuery приймає тільки один аргумент — об'єкт з налаштуваннями. (Старий синтаксис useQuery(key, fn) більше не підтримується).

BasicQuery.tsx
import { useQuery } from '@tanstack/react-query';

function UserProfile({ userId }) {
  const result = useQuery({
    queryKey: ['user', userId], // 🔑 Унікальний ключ
    queryFn: () => fetchUser(userId), // 📡 Функція запиту
  });
  
  // ...
}

Що повертає useQuery?

Об'єкт result містить все, що вам потрібно знати про стан запиту. Ось найважливіші поля:

ПолеТипОпис
dataTDataДані, отримані з сервера. undefined, якщо запит ще не завершився.
errorErrorОб'єкт помилки, якщо запит впав.
isPendingbooleanv5: Запит йде, і даних в кеші немає. Це стан "першого завантаження".
isLoadingbooleanТе саме, що isPending. (В v4 це було інакше, в v5 це аліас).
isErrorbooleanЧи сталася помилка.
isFetchingbooleanЗапит йде прямо зараз (в тому числі фоновий).
Різниця між isPending та isFetching:
  • isPending: "У мене немає даних, покажи скелетон".
  • isFetching: "Я оновлюю дані, покажи маленький спіннер збоку". Якщо у вас є старі дані (Stale), isPending буде false, а isFetchingtrue.

Типовий патерн використання

const { data, isPending, isError, error } = useQuery({ ... });

if (isPending) {
  return <span>Loading...</span>;
}

if (isError) {
  return <span>Error: {error.message}</span>;
}

// Тут TypeScript вже знає, що data визначена
return <div>{data.title}</div>;

Query Keys: Ваші Залежності

queryKey — це масив, який ідентифікує запит. TanStack Query використовує його для:

  1. Кешування даних (це ключ в Map).
  2. Дедуплікації (якщо ключ однаковий, запит не дублюється).
  3. Автоматичного перезапуску (якщо ключ змінився, запит запускається знову).
Думайте про queryKey як про масив залежностей useEffect. Все, що використовується всередині queryFn, має бути в ключі.

Прості ключі

// Список всіх тудушок
['todos']

Ключі з параметрами

// Тудушка з ID 5
['todos', 5]

// Список тудушок, відфільтрований за статусом 'done'
['todos', { status: 'done' }]

Ієрархія ключів (Best Practice)

Ключі мають бути ієрархічними, від загального до конкретного. Це дозволить вам легко керувати групами запитів (наприклад, інвалідувати всі тудушки одразу).

Структура: ['resouce', 'related-id', 'params']

// ✅ Добре
['todos']                 // Всі
['todos', 1]              // Деталі ID 1
['todos', 'list', { page: 1 }] // Список з пагінацією

// ❌ Погано
['todos-1']               // Рядок важко парсити
[1, 'todos']              // Нелогічний порядок

Query Key Factories

Щоб уникнути хаосу і друкарських помилок ("users" vs "user"), професіонали використовують Key Factories.

Створіть файл queries/keys.ts:

queries/keys.ts
export const todoKeys = {
  all: ['todos'] as const,
  lists: () => [...todoKeys.all, 'list'] as const,
  list: (filters: string) => [...todoKeys.lists(), { filters }] as const,
  details: () => [...todoKeys.all, 'detail'] as const,
  detail: (id: number) => [...todoKeys.details(), id] as const,
};

Використання:

useQuery({
  queryKey: todoKeys.list('done'),
  queryFn: ...
})

useQuery({
  queryKey: todoKeys.detail(1),
  queryFn: ...
})

Це дозволяє вам безпечно рефакторити ключі і мати автодоповнення.

Query Function (queryFn)

Функція, яку ви передаєте в queryFn, може бути будь-якою функцією, що повертає Promise.

Вимоги до queryFn:

  1. Повинна повертати Promise, який резолвиться з даними.
  2. Повинна кидати помилку (reject), якщо щось пішло не так.
fetch не кидає помилку на 4xx/5xx статуси (наприклад, 404). Він кидає помилку тільки при проблемах з мережею. Ви мусите самі перевіряти response.ok.
const fetchTodo = async (id: number) => {
  const res = await fetch(`/api/todos/${id}`);
  
  if (!res.ok) {
    throw new Error('Network response was not ok');
  }
  
  return res.json();
};

Axios робить це автоматично, тому він часто зручніший.

Передача параметрів у queryFn

У v5 об'єкт context передається у queryFn автоматично. Він містить queryKey. Це зручно для написання універсальних функцій.

const fetchTodoByContext = async ({ queryKey }) => {
  // queryKey[1] — це наш ID
  const [_key, id] = queryKey;
  const res = await fetch(`/api/todos/${id}`);
  return res.json();
};

useQuery({
  queryKey: ['todos', 5],
  queryFn: fetchTodoByContext, 
});

Але частіше використовують замикання (inline functions):

useQuery({
  queryKey: ['todos', 5],
  queryFn: () => fetchTodo(5), // Простіше і читабельніше
});

Далі: Синхронізація даних (staleTime, gcTime)

Copyright © 2026