Retry для мутаций

Механизм повторных попыток в мутациях TanStack Query определяет, будет ли запрос на изменение данных автоматически перезапущен после ошибки, и при каких условиях это происходит. В отличие от запросов (queries), где retry включён по умолчанию и ориентирован на чтение данных, мутации требуют более осторожного подхода из-за потенциальной необратимости операций.

Повторная попытка мутации управляется на уровне конфигурации useMutation и глобальных настроек QueryClient.


Базовая конфигурация retry в мутациях

Основной параметр, отвечающий за повторные попытки, — retry.

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

const mutation = useMutation({
  mutationFn: async (newUser) => {
    const res = await fetch('/api/users', {
      method: 'POST',
      body: JSON.stringify(newUser),
    })

    if (!res.ok) {
      throw new Error('Ошибка создания пользователя')
    }

    return res.json()
  },

  retry: 2,
})

Значение retry: 2 означает, что при ошибке мутация будет выполнена повторно до двух раз, то есть максимум три попытки (первая + две повторные).

Возможные варианты retry:

  • false — повторные попытки отключены
  • true — бесконечная стратегия по умолчанию (редко используется в мутациях)
  • number — фиксированное количество попыток
  • function — динамическое решение на основе ошибки

Условный retry через функцию

Функциональный вариант позволяет учитывать тип ошибки, код ответа или контекст выполнения.

useMutation({
  mutationFn: createOrder,

  retry: (failureCount, error) => {
    if (error.message.includes('401')) return false
    if (error.message.includes('500') && failureCount < 3) return true
    return false
  },
})

Параметры функции:

  • failureCount — количество неудачных попыток
  • error — объект ошибки, выброшенный mutationFn

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


Задержка между повторными попытками (retryDelay)

По умолчанию задержка между попытками отсутствует или минимальна, но её можно контролировать через retryDelay.

useMutation({
  mutationFn: updateProfile,

  retry: 3,

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

Здесь используется экспоненциальная задержка:

  • 1-я повторная попытка: 1000 мс
  • 2-я: 2000 мс
  • 3-я: 4000 мс
  • максимум ограничен 30000 мс

Такая стратегия снижает нагрузку на сервер при массовых сбоях.


Различие retry в queries и mutations

Поведение retry в мутациях принципиально отличается от запросов:

Queries

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

Mutations

  • изменяют состояние системы
  • retry по умолчанию отсутствует (false)
  • повтор может привести к дублированию операций

Пример риска:

  • создание заказа
  • списание денег
  • отправка email

Если сервер не гарантирует идемпотентность, повтор может привести к нежелательным побочным эффектам.


Идемпотентность и безопасные повторные попытки

Retry в мутациях корректно работает только при наличии идемпотентного API или механизма защиты от дублей.

Типичные подходы:

Idempotency-Key

fetch('/api/payment', {
  method: 'POST',
  headers: {
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify(paymentData),
})

Сервер сохраняет результат первой операции и игнорирует повторные запросы с тем же ключом.


Retry и состояние мутации

Каждая попытка влияет на состояние useMutation:

  • isPending остаётся true между попытками
  • isError устанавливается только после исчерпания retry
  • failureCount увеличивается при каждой ошибке
const mutation = useMutation({
  mutationFn: saveData,
  retry: 2,
  onError: (error, variables, context) => {
    console.log('Ошибка:', error)
  },
})

Важно учитывать, что onError срабатывает только после завершения всех попыток.


Контроль retry на уровне QueryClient

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

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

const queryClient = new QueryClient({
  defaultOptions: {
    mutations: {
      retry: 1,
      retryDelay: 500,
    },
  },
})

Такая конфигурация применяется ко всем мутациям, если они не переопределяют параметры локально.


Retry и mutateAsync

При использовании mutateAsync поведение retry остаётся тем же, но результат становится Promise:

const mutation = useMutation({
  mutationFn: updateUser,
  retry: 2,
})

try {
  const result = await mutation.mutateAsync(userData)
} catch (error) {
  console.log('Окончательная ошибка после retry', error)
}

Promise отклоняется только после завершения всех попыток.


Retry и оптимистические обновления

При использовании optimistic updates retry требует дополнительной синхронизации состояния.

const mutation = useMutation({
  mutationFn: updatePost,

  onMutate: async (newPost) => {
    await queryClient.cancelQueries(['posts'])

    const previous = queryClient.getQueryData(['posts'])

    queryClient.setQueryData(['posts'], (old) => {
      return old.map(p => p.id === newPost.id ? newPost : p)
    })

    return { previous }
  },

  onError: (err, newPost, context) => {
    queryClient.setQueryData(['posts'], context.previous)
  },
})

Если включён retry, onMutate выполняется только один раз, а не на каждую попытку. Повторные вызовы происходят внутри самой мутации, без повторной инициализации optimistic слоя.


Учет типов ошибок при retry

Retry чаще всего применяют к:

  • сетевым ошибкам (fetch failed, timeout)
  • временным 5xx ошибкам
  • перегрузке сервера

Обычно отключают retry для:

  • 4xx ошибок (валидация, авторизация)
  • бизнес-ошибок (дублирование, конфликты)

Пример фильтрации:

retry: (count, error) => {
  const status = error?.status

  if (status >= 400 && status < 500) return false
  if (status >= 500 && count < 2) return true

  return false
}

Retry и конкурирующие мутации

При параллельных мутациях retry может усиливать эффект гонки запросов. Если несколько мутаций обновляют один ресурс:

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

Для стабилизации используется:

  • последовательная очередь мутаций
  • блокировка повторов через mutationKey
  • серверная версионность (ETag, updatedAt)

mutationKey и влияние на retry-логику

mutationKey не управляет retry напрямую, но помогает группировать и контролировать поведение мутаций:

useMutation({
  mutationKey: ['update-user'],
  mutationFn: updateUser,
  retry: 2,
})

В сочетании с кастомным MutationCache можно централизованно управлять стратегиями повторов.


Глобальный MutationCache и retry стратегии

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

const mutationCache = new MutationCache({
  onError: (error, variables, context, mutation) => {
    console.log('Global mutation error')
  },
})

const queryClient = new QueryClient({
  mutationCache,
  defaultOptions: {
    mutations: {
      retry: 1,
    },
  },
})

Такой уровень позволяет централизовать политику устойчивости API-вызовов.


Практическая модель выбора retry стратегии

Поведение retry обычно определяется типом операции:

  • создание ресурсов — чаще retry: false или идемпотентный retry
  • обновление данных — retry: 1–2
  • чтение через мутацию (редкие случаи) — можно использовать стандартные стратегии
  • финансовые операции — только с idempotency key и ограниченным retry

retry и деградация пользовательского опыта

Избыточный retry приводит к:

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

Слишком агрессивное отключение retry приводит к:

  • росту количества ошибок
  • ухудшению устойчивости к нестабильной сети

Баланс достигается через сочетание:

  • ограниченного retry (1–2 попытки)
  • экспоненциальной задержки
  • фильтрации ошибок по типу
  • серверной идемпотентности