Работа с Fetch API

Работа 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-логики

При увеличении количества запросов повторяющаяся логика обработки ответа и ошибок быстро разрастается. Практика выделения универсальной обертки над 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}`)

Такой подход снижает дублирование и упрощает обработку ошибок на уровне всей системы.


Передача параметров в queryFn

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 params через URLSearchParams

Для фильтрации и пагинации часто используются 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 требует стабильности ссылки или сериализации, иначе возможны лишние перезапросы.


Управление AbortController

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()
}

Отмена запросов особенно важна при быстром переключении фильтров и навигации, так как предотвращает гонки данных.


Повторные запросы и Fetch API

TanStack Query по умолчанию может повторять запросы при ошибках. Fetch API сам по себе не содержит retry-механизма, поэтому управление повторными попытками полностью остается на стороне библиотеки.

useQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts,
  retry: 3,
  retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000)
})

При этом важно учитывать, что повторные запросы с Fetch API не должны приводить к нежелательным побочным эффектам на сервере. Поэтому безопаснее использовать GET-запросы для автоматического retry.


Параллельные запросы через Fetch API

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 агрегирует состояния загрузки и ошибок.


Дедупликация Fetch-запросов

При множественном использовании одного и того же queryKey TanStack Query автоматически предотвращает дублирование запросов. Fetch API при этом вызывается только один раз.

useQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts
})

useQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts
})

Обе подписки используют общий кэш, а Fetch API не выполняется повторно до истечения времени актуальности данных.


Кэширование и staleTime

Fetch API всегда возвращает свежие данные, но TanStack Query добавляет слой кэширования.

useQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts,
  staleTime: 1000 * 60 * 5
})

В течение staleTime повторные обращения к данным не вызывают Fetch API, что снижает нагрузку на сеть.


Интеграция с POST-запросами через mutation

Хотя 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 API в TanStack Query

Основные проблемы возникают не в самой библиотеке, а в неправильной организации fetch-логики:

  1. Отсутствие проверки response.ok
  2. Нестабильные queryKey при использовании объектов
  3. Игнорирование AbortSignal
  4. Отсутствие централизованной обработки ошибок
  5. Повторяющиеся реализации fetch-оберток

Стабильная архитектура запросов строится вокруг единообразного baseFetch, строгого контроля ключей и использования встроенного жизненного цикла TanStack Query.