query function — это функция, которая отвечает за
получение данных в TanStack Query. Именно она выполняет HTTP-запрос,
обращается к API, читает данные из IndexedDB, localStorage или любого
другого источника.
TanStack Query не знает, откуда брать данные. Библиотека управляет состоянием запроса, кешированием, повторными попытками, синхронизацией и обновлением, но фактическое получение данных полностью лежит на query function.
Базовая структура выглядит следующим образом:
useQuery({
queryKey: ['users'],
queryFn: fetchUsers
});
В этом примере:
queryKey идентифицирует запрос;queryFn выполняет получение данных.Наиболее распространённый вариант — использование
fetch.
async function fetchUsers() {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error('Ошибка загрузки пользователей');
}
return response.json();
}
Использование:
const query = useQuery({
queryKey: ['users'],
queryFn: fetchUsers
});
Ключевой момент заключается в том, что query function обязана:
Функцию можно передавать непосредственно внутрь
useQuery.
const query = useQuery({
queryKey: ['users'],
queryFn: async () => {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error('Ошибка загрузки');
}
return response.json();
}
});
Такой подход удобен для небольших запросов.
Недостатки:
Поэтому в крупных проектах query functions обычно выносят отдельно.
TanStack Query не зависит от HTTP-клиента.
Вместо fetch можно использовать axios.
import axios from 'axios';
async function fetchUsers() {
const response = await axios.get('/api/users');
return response.data;
}
Использование:
const query = useQuery({
queryKey: ['users'],
queryFn: fetchUsers
});
Query function должна вернуть итоговые данные, которые попадут в
data.
const query = useQuery({
queryKey: ['users'],
queryFn: fetchUsers
});
console.log(query.data);
Если функция возвращает:
return response.data;
то именно это значение станет содержимым query.data.
Ошибки должны выбрасываться через throw.
async function fetchUsers() {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error('Ошибка сервера');
}
return response.json();
}
Тогда TanStack Query автоматически:
error;isError.Если query function выбрасывает ошибку:
throw new Error('Unauthorized');
то состояние запроса становится:
{
status: 'error',
isError: true,
error: Error
}
Пример:
const {
data,
error,
isError
} = useQuery({
queryKey: ['users'],
queryFn: fetchUsers
});
if (isError) {
return <div>{error.message}</div>;
}
TanStack Query автоматически передаёт объект контекста в query function.
useQuery({
queryKey: ['user', 15],
queryFn: ({ queryKey }) => {
console.log(queryKey);
}
});
Контекст содержит:
queryKeysignalmetapageParam (для infinite queries)Наиболее частое применение context — извлечение параметров из query key.
useQuery({
queryKey: ['user', userId],
queryFn: ({ queryKey }) => {
const [, id] = queryKey;
return fetchUser(id);
}
});
Функция получения пользователя:
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error('Пользователь не найден');
}
return response.json();
}
Неправильный подход:
useQuery({
queryKey: ['user'],
queryFn: () => fetchUser(userId)
});
Проблема заключается в том, что queryKey не содержит
userId.
Это приводит к:
Правильный вариант:
useQuery({
queryKey: ['user', userId],
queryFn: ({ queryKey }) => {
const [, id] = queryKey;
return fetchUser(id);
}
});
TanStack Query поддерживает AbortController.
В query function передаётся signal.
useQuery({
queryKey: ['users'],
queryFn: async ({ signal }) => {
const response = await fetch('/api/users', {
signal
});
return response.json();
}
});
Запрос может быть отменён при:
При использовании fetch отменённый запрос вызывает
ошибку AbortError.
async function fetchUsers({ signal }) {
const response = await fetch('/api/users', {
signal
});
return response.json();
}
Обычно отдельно обрабатывать её не требуется — TanStack Query делает это автоматически.
Query function должна быть максимально чистой и предсказуемой.
Плохой пример:
let counter = 0;
async function fetchUsers() {
counter++;
return api.getUsers(counter);
}
Проблемы:
Правильный подход — разделение API-слоя.
// api/users.js
export async function getUsers() {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error('Ошибка загрузки');
}
return response.json();
}
Использование:
import { getUsers } from './api/users';
useQuery({
queryKey: ['users'],
queryFn: getUsers
});
В больших проектах применяются фабрики query functions.
function createUserQuery(userId) {
return {
queryKey: ['user', userId],
queryFn: () => fetchUser(userId)
};
}
Использование:
const query = useQuery(createUserQuery(15));
Если функция выбрасывает ошибку, TanStack Query может автоматически повторить запрос.
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
retry: 3
});
При ошибке библиотека:
Retry работает только при ошибке.
Если query function успешно вернула данные:
return data;
повторных запросов не будет.
Иногда retry необходимо отключать для определённых ошибок.
Например:
useQuery({
queryKey: ['profile'],
queryFn: fetchProfile,
retry: (failureCount, error) => {
if (error.status === 401) {
return false;
}
return failureCount < 3;
}
});
В query function можно получить дополнительные метаданные.
useQuery({
queryKey: ['users'],
meta: {
requiresAuth: true
},
queryFn: ({ meta }) => {
console.log(meta.requiresAuth);
}
});
Query function может использовать:
async/await;Пример с Promise:
function fetchUsers() {
return fetch('/api/users')
.then(response => {
if (!response.ok) {
throw new Error('Ошибка');
}
return response.json();
});
}
Пример с GraphQL:
async function fetchPosts() {
const response = await fetch('/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: `
query {
posts {
id
title
}
}
`
})
});
const result = await response.json();
return result.data.posts;
}
Иногда необходимо выполнить несколько запросов одновременно.
async function fetchDashboardData() {
const [
users,
posts,
comments
] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json())
]);
return {
users,
posts,
comments
};
}
Query function может преобразовывать данные перед возвратом.
async function fetchUsers() {
const response = await fetch('/api/users');
const users = await response.json();
return users.map(user => ({
id: user.id,
fullName: `${user.firstName} ${user.lastName}`
}));
}
Сложную трансформацию лучше выносить в select.
Плохой пример:
queryFn: async () => {
const data = await fetchUsers();
return heavyTransformation(data);
}
Причины:
В infinite query появляется pageParam.
useInfiniteQuery({
queryKey: ['posts'],
queryFn: async ({ pageParam = 1 }) => {
const response = await fetch(
`/api/posts?page=${pageParam}`
);
return response.json();
},
getNextPageParam: lastPage => {
return lastPage.nextPage;
}
});
Неправильно:
if (!response.ok) {
return null;
}
Правильно:
if (!response.ok) {
throw new Error('Ошибка');
}
Неправильно:
queryKey: ['user']
если запрос зависит от userId.
Правильно:
queryKey: ['user', userId]
Query function не должна:
Плохой пример:
async function fetchUsers() {
alert('Запрос');
return api.getUsers();
}
В крупной архитектуре query functions становятся центральной частью data layer.
Типичная структура:
src/
├── api/
│ ├── users.js
│ ├── posts.js
│ └── comments.js
├── queries/
│ ├── users.js
│ ├── posts.js
│ └── comments.js
Пример:
// api/users.js
export async function getUsers() {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error('Ошибка');
}
return response.json();
}
// queries/users.js
import { getUsers } from '../api/users';
export function usersQuery() {
return {
queryKey: ['users'],
queryFn: getUsers
};
}
Использование:
const query = useQuery(usersQuery());
Разделение логики даёт: