Query function в TanStack Query — это центральный элемент, отвечающий за получение данных. Именно она определяет, как, откуда и в каком формате данные попадают в кэш библиотеки. Несмотря на внешнюю простоту, структура query function имеет несколько важных нюансов, напрямую влияющих на предсказуемость кэширования, обработку ошибок, повторные запросы и масштабируемость приложения.
В своей основе query function — это обычная асинхронная функция, которая возвращает данные:
const fetchUsers = async () => {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error('Ошибка загрузки пользователей');
}
return response.json();
};
Эта функция передаётся в queryFn внутри
useQuery:
import { useQuery } fr om '@tanstack/react-query';
const usersQuery = useQuery({
queryKey: ['users'],
queryFn: fetchUsers
});
Ключевой момент: TanStack Query не требует специального формата возврата. Достаточно вернуть значение или Promise, который его резолвит.
Query function в TanStack Query подчиняется простому контракту:
Это разделение ответственности критично: TanStack Query управляет состоянием, а query function — только I/O логикой.
TanStack Query передаёт в query function один аргумент — объект с контекстом запроса:
const fetchUserById = async ({ queryKey }) => {
const [, id] = queryKey;
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error('Ошибка загрузки пользователя');
}
return response.json();
};
Использование:
useQuery({
queryKey: ['user', userId],
queryFn: fetchUserById
});
Контекст query function может включать:
queryKey — ключ запросаsignal — AbortSignal для отмены запросаmeta — пользовательские метаданныеПример использования signal:
const fetchUsers = async ({ signal }) => {
const response = await fetch('/api/users', { signal });
if (!response.ok) {
throw new Error('Ошибка загрузки');
}
return response.json();
};
Это особенно важно для отмены запросов при смене компонентов или ключей.
queryKey — это источник параметров для запроса. TanStack
Query не передаёт отдельные аргументы, поэтому вся параметризация
строится через структуру ключа.
Пример:
useQuery({
queryKey: ['posts', { page: 1, lim it: 10 }],
queryFn: fetchPosts
});
Query function:
const fetchPosts = async ({ queryKey }) => {
const [, params] = queryKey;
const response = await fetch(
`/api/posts?page=${params.page}&limit=${params.limit}`
);
if (!response.ok) {
throw new Error('Ошибка загрузки постов');
}
return response.json();
};
Одно из ключевых требований — детерминированность относительно
queryKey.
Для одного и того же queryKey query function должна
возвращать одинаковый результат (с точки зрения структуры данных), иначе
нарушается логика кэширования.
Нарушения включают:
Math.random)Корректный подход:
const fetchFeed = async ({ queryKey }) => {
const [, timestamp] = queryKey;
const response = await fetch(`/api/feed?ts=${timestamp}`);
return response.json();
};
Любое исключение в query function трактуется как ошибка запроса.
const fetchData = async () => {
const res = await fetch('/api/data');
if (!res.ok) {
throw new Error(`HTTP error: ${res.status}`);
}
return res.json();
};
TanStack Query перехватывает такие ошибки и переводит query в
состояние error.
Важно: возврат null или undefined не
считается ошибкой. Ошибка должна быть именно через
throw.
Query function может быть как синхронной, так и асинхронной:
const syncQueryFn = () => {
return { value: 42 };
};
или
const asyncQueryFn = async () => {
const res = await fetch('/api/value');
return res.json();
};
Однако в реальных приложениях синхронные функции используются редко, так как основной сценарий — работа с сетью или I/O.
Query function может вызываться несколько раз в следующих случаях:
queryKeyrefetchПоэтому query function должна быть идемпотентной с точки зрения побочных эффектов.
Нежелательно:
const fetchData = async () => {
console.log('Запрос отправлен');
return fetch('/api/data').then(r => r.json());
};
Логирование допустимо, но любые побочные изменения состояния вне запроса — нет.
TanStack Query передаёт signal, позволяющий отменять
запросы при устаревании:
const fetchUsers = async ({ signal }) => {
const res = await fetch('/api/users', { signal });
return res.json();
};
При смене queryKey предыдущий запрос может быть
автоматически отменён. Это снижает нагрузку на сеть и предотвращает
гонки данных.
На практике query function часто выступает слоем адаптации между API и приложением:
const fetchUser = async ({ queryKey }) => {
const [, id] = queryKey;
const res = await fetch(`/api/users/${id}`);
if (!res.ok) {
throw new Error('User not found');
}
const data = await res.json();
return {
id: data.id,
fullName: `${data.firstName} ${data.lastName}`,
isActive: data.status === 'active'
};
};
Такой подход позволяет:
Частые проблемы:
Query function не должна знать о UI-логике:
// плохо
const fetchUsers = async () => {
return fetch('/api/users').then(r => r.json()).then(data =>
data.map(u => ({ ...u, selected: false }))
);
};
Состояние UI должно формироваться отдельно.
// плохо
const fetchPosts = async (page) => {
return fetch(`/api/posts?page=${page}`);
};
Правильно — через queryKey.
// плохо
const fetchData = async () => {
localStorage.setItem('lastFetch', Date.now());
return fetch('/api/data');
};
Query function должна оставаться чистой относительно внешнего состояния.
В TypeScript структура query function может быть явно описана:
import { QueryFunctionContext } from '@tanstack/react-query';
type User = {
id: number;
name: string;
};
const fetchUser = async (
context: QueryFunctionContext<['user', number]>
): Promise<User> => {
const [, id] = context.queryKey;
const res = await fetch(`/api/users/${id}`);
return res.json();
};
Это усиливает предсказуемость и снижает риск ошибок при изменении
queryKey.
Query functions могут быть переиспользуемыми и композиционными:
const createFetcher = (baseUrl) => {
return async ({ queryKey }) => {
const [, path] = queryKey;
const res = await fetch(`${baseUrl}/${path}`);
return res.json();
};
};
const fetchApi = createFetcher('/api');
useQuery({
queryKey: ['api', 'users'],
queryFn: fetchApi
});
Такой подход удобен для:
Хотя query function не управляет кэшем напрямую, её структура влияет на:
Неправильная структура query function часто приводит к:
Поэтому query function рассматривается не как изолированная функция,
а как часть контрактной системы вместе с queryKey.