Механизм повторных попыток в мутациях TanStack Query определяет, будет ли запрос на изменение данных автоматически перезапущен после ошибки, и при каких условиях это происходит. В отличие от запросов (queries), где retry включён по умолчанию и ориентирован на чтение данных, мутации требуют более осторожного подхода из-за потенциальной необратимости операций.
Повторная попытка мутации управляется на уровне конфигурации
useMutation и глобальных настроек
QueryClient.
Основной параметр, отвечающий за повторные попытки, —
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 — динамическое решение на основе ошибкиФункциональный вариант позволяет учитывать тип ошибки, код ответа или контекст выполнения.
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.
useMutation({
mutationFn: updateProfile,
retry: 3,
retryDelay: (attemptIndex) => {
return Math.min(1000 * 2 ** attemptIndex, 30000)
},
})
Здесь используется экспоненциальная задержка:
Такая стратегия снижает нагрузку на сервер при массовых сбоях.
Поведение retry в мутациях принципиально отличается от запросов:
false)Пример риска:
Если сервер не гарантирует идемпотентность, повтор может привести к нежелательным побочным эффектам.
Retry в мутациях корректно работает только при наличии идемпотентного API или механизма защиты от дублей.
Типичные подходы:
fetch('/api/payment', {
method: 'POST',
headers: {
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify(paymentData),
})
Сервер сохраняет результат первой операции и игнорирует повторные запросы с тем же ключом.
Каждая попытка влияет на состояние useMutation:
isPending остаётся true между
попыткамиisError устанавливается только после исчерпания
retryfailureCount увеличивается при каждой ошибкеconst mutation = useMutation({
mutationFn: saveData,
retry: 2,
onError: (error, variables, context) => {
console.log('Ошибка:', error)
},
})
Важно учитывать, что onError срабатывает только после
завершения всех попыток.
Глобальные настройки позволяют задать стратегию по умолчанию:
import { QueryClient } from '@tanstack/react-query'
const queryClient = new QueryClient({
defaultOptions: {
mutations: {
retry: 1,
retryDelay: 500,
},
},
})
Такая конфигурация применяется ко всем мутациям, если они не переопределяют параметры локально.
При использовании mutateAsync поведение retry остаётся
тем же, но результат становится Promise:
const mutation = useMutation({
mutationFn: updateUser,
retry: 2,
})
try {
const result = await mutation.mutateAsync(userData)
} catch (error) {
console.log('Окончательная ошибка после retry', error)
}
Promise отклоняется только после завершения всех попыток.
При использовании 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 чаще всего применяют к:
fetch failed,
timeout)Обычно отключают retry для:
Пример фильтрации:
retry: (count, error) => {
const status = error?.status
if (status >= 400 && status < 500) return false
if (status >= 500 && count < 2) return true
return false
}
При параллельных мутациях retry может усиливать эффект гонки запросов. Если несколько мутаций обновляют один ресурс:
Для стабилизации используется:
mutationKeymutationKey не управляет retry напрямую, но помогает
группировать и контролировать поведение мутаций:
useMutation({
mutationKey: ['update-user'],
mutationFn: updateUser,
retry: 2,
})
В сочетании с кастомным MutationCache можно
централизованно управлять стратегиями повторов.
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: false или идемпотентный
retryretry: 1–2Избыточный retry приводит к:
Слишком агрессивное отключение retry приводит к:
Баланс достигается через сочетание: