Работа TanStack Query с сетевыми запросами строится вокруг функции
queryFn. Именно она отвечает за получение данных, а
библиотека берет на себя кэширование, повторные запросы, синхронизацию
состояния и фоновые обновления.
Fetch API в этом контексте выступает как стандартный инструмент браузера для выполнения HTTP-запросов, и его использование в связке с TanStack Query является наиболее прямолинейным вариантом интеграции.
import { useQuery } fr om '@tanstack/react-query'
async function fetchPosts() {
const response = await fetch('https://api.example.com/posts')
if (!response.ok) {
throw new Error('Ошибка загрузки данных')
}
return response.json()
}
export function Posts() {
const { data, isLoading, error } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts
})
if (isLoading) return 'Загрузка...'
if (error) return 'Ошибка загрузки'
return (
<ul>
{data.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
Fetch API не выбрасывает ошибку при HTTP-статусах 4xx и 5xx, поэтому
обязательна проверка response.ok. Без нее TanStack Query не
сможет корректно различить успешный и неуспешный результат.
При увеличении количества запросов повторяющаяся логика обработки ответа и ошибок быстро разрастается. Практика выделения универсальной обертки над Fetch API позволяет стандартизировать поведение всех запросов.
export async function baseFetch(url, options) {
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
...(options?.headers || {})
},
...options
})
if (!response.ok) {
const errorBody = await response.text()
throw new Error(errorBody || 'Request failed')
}
return response.json()
}
Далее эта функция становится базой для всех queryFn:
const fetchUsers = () => baseFetch('https://api.example.com/users')
const fetchUserById = (id) => baseFetch(`https://api.example.com/users/${id}`)
Такой подход снижает дублирование и упрощает обработку ошибок на уровне всей системы.
TanStack Query передает параметры через queryKey. Это
ключевой механизм для построения динамических запросов, зависящих от
входных данных.
const fetchUser = async ({ queryKey }) => {
const [, userId] = queryKey
return baseFetch(`https://api.example.com/users/${userId}`)
}
useQuery({
queryKey: ['user', 42],
queryFn: fetchUser
})
Любое изменение queryKey приводит к автоматическому
повторному запросу. Это делает Fetch API-запросы полностью
декларативными.
Для фильтрации и пагинации часто используются query-параметры URL. Их
корректная сборка критична для кэширования, так как TanStack Query
различает запросы по queryKey.
const fetchPosts = async ({ queryKey }) => {
const [, { page, lim it }] = queryKey
const params = new URLSearchParams({
page,
limit
})
return baseFetch(`https://api.example.com/posts?${params}`)
}
useQuery({
queryKey: ['posts', { page: 1, limit: 10 }],
queryFn: fetchPosts
})
Использование объекта в queryKey требует стабильности
ссылки или сериализации, иначе возможны лишние перезапросы.
Fetch API поддерживает отмену запросов через
AbortController. TanStack Query автоматически передает
signal в queryFn, что позволяет корректно
прерывать запросы при смене ключа или размонтировании компонента.
const fetchPosts = async ({ signal }) => {
return baseFetch('https://api.example.com/posts', { signal })
}
Расширенная версия:
async function baseFetch(url, options = {}) {
const response = await fetch(url, {
...options,
signal: options.signal
})
if (!response.ok) {
throw new Error('Ошибка запроса')
}
return response.json()
}
Отмена запросов особенно важна при быстром переключении фильтров и навигации, так как предотвращает гонки данных.
TanStack Query по умолчанию может повторять запросы при ошибках. Fetch API сам по себе не содержит retry-механизма, поэтому управление повторными попытками полностью остается на стороне библиотеки.
useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
retry: 3,
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000)
})
При этом важно учитывать, что повторные запросы с Fetch API не должны приводить к нежелательным побочным эффектам на сервере. Поэтому безопаснее использовать GET-запросы для автоматического retry.
TanStack Query позволяет выполнять несколько независимых запросов параллельно, каждый из которых использует Fetch API.
const useDashboard = () => {
const posts = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts
})
const users = useQuery({
queryKey: ['users'],
queryFn: fetchUsers
})
return { posts, users }
}
Fetch API в этом случае работает изолированно в каждом
queryFn, а TanStack Query агрегирует состояния загрузки и
ошибок.
При множественном использовании одного и того же
queryKey TanStack Query автоматически предотвращает
дублирование запросов. Fetch API при этом вызывается только один
раз.
useQuery({
queryKey: ['posts'],
queryFn: fetchPosts
})
useQuery({
queryKey: ['posts'],
queryFn: fetchPosts
})
Обе подписки используют общий кэш, а Fetch API не выполняется повторно до истечения времени актуальности данных.
Fetch API всегда возвращает свежие данные, но TanStack Query добавляет слой кэширования.
useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
staleTime: 1000 * 60 * 5
})
В течение staleTime повторные обращения к данным не
вызывают Fetch API, что снижает нагрузку на сеть.
Хотя Fetch API чаще используется для получения данных, он также
применяется в mutationFn.
async function createPost(newPost) {
const response = await fetch('https://api.example.com/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(newPost)
})
if (!response.ok) {
throw new Error('Ошибка создания поста')
}
return response.json()
}
import { useMutation, useQueryClient } from '@tanstack/react-query'
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: createPost,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] })
}
})
Fetch API в mutation используется синхронно с механизмом инвалидирования кэша.
Fetch API не поддерживает таймауты нативно, поэтому они реализуются через AbortController.
function fetchWithTimeout(url, timeout = 5000) {
const controller = new AbortController()
const timer = setTimeout(() => {
controller.abort()
}, timeout)
return fetch(url, { signal: controller.signal })
.finally(() => clearTimeout(timer))
}
useQuery({
queryKey: ['posts'],
queryFn: () => fetchWithTimeout('https://api.example.com/posts', 3000)
})
Основные проблемы возникают не в самой библиотеке, а в неправильной организации fetch-логики:
response.okqueryKey при использовании объектовAbortSignalСтабильная архитектура запросов строится вокруг единообразного
baseFetch, строгого контроля ключей и использования
встроенного жизненного цикла TanStack Query.