Кастомизация логики повторов

Повторные попытки выполнения запросов в TanStack Query являются встроенным механизмом устойчивости к сетевым сбоям и временным ошибкам серверной стороны. Логика retry применяется как для query, так и для mutation, но настраивается отдельно и может быть переопределена на разных уровнях: глобально через QueryClient, локально через конкретный запрос или мутацию, а также динамически через функцию, принимающую контекст ошибки.

В стандартной конфигурации TanStack Query автоматически повторяет неуспешные запросы несколько раз. Поведение определяется следующими параметрами:

  • retry — количество попыток или функция-решатель
  • retryDelay — задержка между попытками
  • тип ошибки — влияет на то, будет ли повтор выполняться

По умолчанию библиотека использует ограниченное число повторов (обычно 3) для ошибок сети или временных сбоев.

Повтор не выполняется, если:

  • запрос был отменён
  • ошибка явно помечена как не подлежащая повтору
  • retry установлен в false

Конфигурация retry на уровне QueryClient

Глобальная настройка определяет поведение всех запросов по умолчанию:

import { QueryClient } from '@tanstack/react-query'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: 3,
      retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
    },
  },
})

Здесь используется экспоненциальная задержка, при которой интервал увеличивается с каждой попыткой. Такая стратегия снижает нагрузку на сервер при массовых сбоях и уменьшает вероятность повторного отказа.

Локальное переопределение поведения retry

Любой query может переопределить глобальные настройки:

useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  retry: 1,
})

или полностью отключить повторы:

useQuery({
  queryKey: ['auth'],
  queryFn: fetchSession,
  retry: false,
})

Отключение retry часто используется для:

  • авторизационных запросов
  • операций, где повтор может привести к побочным эффектам
  • запросов с высокой стоимостью выполнения

Функциональный контроль retry

Наиболее гибкий вариант — передача функции, принимающей количество попыток и ошибку:

useQuery({
  queryKey: ['profile'],
  queryFn: fetchProfile,
  retry: (failureCount, error) => {
    if (error.status === 404) return false
    if (failureCount > 5) return false
    return true
  },
})

Функция позволяет реализовать условную логику:

  • игнорировать повтор при конкретных HTTP-статусах
  • ограничивать количество попыток
  • учитывать тип ошибки (network, server, parsing)

Различие поведения retry для query и mutation

Повторы для query и mutation имеют принципиальные различия.

Query:

  • автоматически кэшируются
  • повторяются при инициализации и refetch
  • предназначены для безопасных операций чтения

Mutation:

  • по умолчанию могут повторяться, но используются осторожнее
  • часто отключаются для предотвращения дублирования действий
useMutation({
  mutationFn: createUser,
  retry: 0,
})

Для mutation повтор может привести к:

  • дублированию записей
  • повторной отправке платежей
  • неконсистентному состоянию сервера

retryDelay и стратегии задержки

Параметр retryDelay управляет интервалом между попытками.

Статическая задержка:

retryDelay: 2000

Экспоненциальная стратегия:

retryDelay: attemptIndex =>
  Math.min(1000 * 2 ** attemptIndex, 30000)

Добавление случайного jitter:

retryDelay: attemptIndex =>
  Math.min(1000 * 2 ** attemptIndex, 30000) *
  (0.5 + Math.random())

Jitter снижает вероятность «thundering herd problem», когда множество клиентов одновременно повторяют запросы после сбоя.

Условное отключение retry по типу ошибки

Часто требуется различать временные и постоянные ошибки:

useQuery({
  queryKey: ['post', id],
  queryFn: fetchPost,
  retry: (failureCount, error) => {
    if (error?.status >= 400 && error?.status < 500) {
      return false
    }
    return failureCount < 3
  },
})

Клиентские ошибки (4xx) обычно не имеют смысла для повторов, тогда как серверные (5xx) или сетевые сбои являются кандидатами на retry.

Интеграция retry с состоянием сети

TanStack Query учитывает состояние онлайн/офлайн, но retry может комбинироваться с этим поведением.

При восстановлении соединения:

  • запросы могут автоматически refetch
  • retry может продолжить выполнение очереди попыток

Настройка networkMode влияет на это поведение:

useQuery({
  queryKey: ['dashboard'],
  queryFn: fetchDashboard,
  networkMode: 'online',
  retry: 2,
})

Режимы позволяют контролировать, выполняются ли попытки при отсутствии сети или откладываются до восстановления соединения.

Retry и отмена запросов

Повтор не выполняется, если запрос был отменён через AbortController:

const queryFn = async ({ signal }) => {
  const res = await fetch('/api/data', { signal })
  return res.json()
}

При отмене:

  • текущая попытка прерывается
  • последующие retry не запускаются автоматически

Это важно для предотвращения утечек ресурсов при быстрых переключениях UI.

Разделение retry по средам и условиям выполнения

Часто требуется разное поведение для development и production:

const isDev = process.env.NODE_ENV === 'development'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: isDev ? 0 : 3,
    },
  },
})

В development повторные запросы могут скрывать реальные ошибки и замедлять отладку, поэтому их обычно отключают.

Retry в сочетании с staleTime и cache

Поведение retry влияет на UX только при отсутствии актуальных данных в кеше. Если данные свежие:

  • retry может не запускаться из-за использования cache
  • refetch может быть отложен

Конфигурация:

useQuery({
  queryKey: ['settings'],
  queryFn: fetchSettings,
  staleTime: 60000,
  retry: 2,
})

При высокой staleTime вероятность запуска retry снижается, так как библиотека предпочитает использовать кэш.

Практика ограничения retry при высокой нагрузке

В системах с высоким трафиком чрезмерные retry могут усугубить нагрузку. Используются ограничения:

  • уменьшение retry до 1–2
  • увеличение retryDelay
  • добавление jitter
  • отключение retry для второстепенных данных
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: 1,
      retryDelay: attempt =>
        Math.min(2000 * attempt, 10000),
    },
  },
})

Логирование и наблюдение за retry

Retry-логика часто сопровождается мониторингом:

useQuery({
  queryKey: ['analytics'],
  queryFn: fetchAnalytics,
  retry: (count, error) => {
    console.log('retry attempt', count, error)
    return count < 3
  },
})

В продакшене подобная логика обычно заменяется интеграцией с системами наблюдаемости, где фиксируются:

  • количество повторов
  • тип ошибки
  • время восстановления

Кастомные сценарии управления retry

Продвинутые сценарии включают:

  • отключение retry для определённых endpoint-ов
  • динамическое изменение retry на основе заголовков ответа
  • зависимость retry от пользовательского состояния (например, premium/standard)
retry: (failureCount, error) => {
  if (error?.response?.headers?.['x-no-retry'] === 'true') {
    return false
  }
  return failureCount < 3
}

Такая модель позволяет серверу участвовать в управлении клиентским поведением.

Итоговая модель поведения retry

Логика повторов в TanStack Query представляет собой многоуровневую систему, где решение о повторе формируется на основе:

  • глобальных настроек QueryClient
  • локальных параметров query/mutation
  • типа и структуры ошибки
  • количества уже выполненных попыток
  • сетевого состояния
  • пользовательской логики через функции

Эта гибкость позволяет адаптировать поведение запросов под любые сценарии — от высоконагруженных API до клиентских интерфейсов с критичными операциями.