Сетевые запросы нестабильны по своей природе. Даже корректно написанный backend может временно возвращать ошибки из-за перегрузки, проблем сети, балансировщиков, таймаутов или ограничений API. TanStack Query содержит встроенный механизм повторных запросов, который автоматически пытается повторить неудачный запрос без участия пользователя.
Retry-механизм особенно полезен в следующих ситуациях:
При этом неправильная стратегия retry способна привести к противоположному эффекту:
По умолчанию TanStack Query автоматически повторяет failed-запросы.
const query = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
})
Если fetchPosts() завершится ошибкой, библиотека
выполнит повторный запрос автоматически.
Стандартное поведение:
3Алгоритм выглядит следующим образом:
Выполняется запрос
Если запрос успешен — данные сохраняются
Если произошла ошибка:
Если retries исчерпаны — ошибка окончательно попадает в компонент
Количество retry настраивается через свойство retry.
useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
retry: false,
})
При первой ошибке запрос завершится окончательно.
Такой подход полезен:
useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
retry: 5,
})
TanStack Query выполнит максимум 5 повторных запросов.
Наиболее гибкий вариант — использование callback-функции.
useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
retry: (failureCount, error) => {
if (error.status === 404) {
return false
}
if (error.status === 401) {
return false
}
return failureCount < 3
},
})
Аргументы:
| Аргумент | Описание |
|---|---|
| failureCount | количество ошибок подряд |
| error | объект ошибки |
Это позволяет строить сложные стратегии retry.
Очень распространённый паттерн.
useQuery({
queryKey: ['profile'],
queryFn: fetchProfile,
retry: (count, error) => {
return error.code === 'NETWORK_ERROR' && count < 5
},
})
Retry выполняется исключительно при проблемах сети.
Ошибки клиента обычно не имеют смысла для повторного выполнения.
retry: (count, error) => {
if (error.status >= 400 && error.status < 500) {
return false
}
return count < 3
}
Причины:
Повторный запрос ничего не изменит.
Ошибки сервера чаще являются временными.
retry: (count, error) => {
return error.status >= 500 && count < 5
}
Особенно полезно для:
Между повторными запросами TanStack Query делает паузу.
Настройка:
useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
retryDelay: 1000,
})
Каждый retry будет ожидать 1 секунду.
Более мощный вариант.
useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
retryDelay: (attempt) => {
return attempt * 1000
},
})
Поведение:
| Попытка | Delay |
|---|---|
| 1 | 1000ms |
| 2 | 2000ms |
| 3 | 3000ms |
Наиболее популярная стратегия retry.
С каждым retry задержка увеличивается экспоненциально.
retryDelay: (attempt) => {
return Math.min(1000 * 2 ** attempt, 30000)
}
Логика:
| Попытка | Delay |
|---|---|
| 1 | 2000ms |
| 2 | 4000ms |
| 3 | 8000ms |
| 4 | 16000ms |
| 5 | 30000ms |
Такая стратегия:
Линейное увеличение задержки.
retryDelay: (attempt) => attempt * 2000
Пример:
| Попытка | Delay |
|---|---|
| 1 | 2s |
| 2 | 4s |
| 3 | 6s |
Используется реже, чем exponential backoff.
Фиксированный интервал.
retryDelay: 3000
Каждый retry выполняется через 3 секунды.
Подходит:
Если тысячи клиентов начинают retry одновременно, возникает эффект retry storm.
Для предотвращения используют jitter.
retryDelay: (attempt) => {
const baseDelay = 1000 * 2 ** attempt
const jitter = Math.random() * 1000
return baseDelay + jitter
}
Преимущества:
Настройка через QueryClient.
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 3,
retryDelay: (attempt) => {
return Math.min(1000 * 2 ** attempt, 30000)
},
},
},
})
Все queries автоматически получают общую стратегию.
Глобальные настройки можно изменить в конкретном query.
useQuery({
queryKey: ['critical-data'],
queryFn: fetchCriticalData,
retry: 10,
})
Локальные параметры имеют приоритет.
TanStack Query умеет учитывать состояние сети.
При offline-состоянии retry может быть отложен до восстановления соединения.
networkMode: 'online'
Режимы:
| Режим | Поведение |
|---|---|
| online | запросы только при наличии сети |
| always | игнорирует offline |
| offlineFirst | оптимизация под offline |
После reconnect query может автоматически перезапуститься.
useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
retry: 5,
refetchOnReconnect: true,
})
Это особенно важно для мобильных приложений.
При возврате вкладки в фокус TanStack Query также может повторно выполнить запрос.
refetchOnWindowFocus: true
Комбинация:
создаёт очень устойчивую систему загрузки данных.
Retry не заменяет timeout.
Без timeout запрос может зависнуть навсегда.
Правильный подход:
const fetchPosts = async () => {
const controller = new AbortController()
const timeout = setTimeout(() => {
controller.abort()
}, 5000)
try {
const response = await fetch('/api/posts', {
signal: controller.signal,
})
return response.json()
} finally {
clearTimeout(timeout)
}
}
Retry будет работать только после завершения timeout.
TanStack Query автоматически передаёт signal.
const fetchPosts = async ({ signal }) => {
const response = await fetch('/api/posts', {
signal,
})
return response.json()
}
Это предотвращает:
Mutations по умолчанию retry не используют.
Причина — mutation может быть неидемпотентной.
Например:
createOrder()
Повторный вызов способен:
Тем не менее retry можно включить вручную.
useMutation({
mutationFn: updateProfile,
retry: 2,
})
Подходит для:
POST требует осторожности.
Плохой пример:
retry: 5
Без idempotency backend может выполнить операцию несколько раз.
Правильные решения:
Некоторые API возвращают:
429 Too Many Requests
В таких случаях retry должен учитывать заголовки сервера.
retry: (count, error) => {
return error.status === 429 && count < 5
}
Backend может сообщать рекомендуемую задержку.
Retry-After: 30
Можно использовать её динамически.
retryDelay: (_, error) => {
return error.retryAfter * 1000
}
Иногда retry зависит от бизнес-логики.
retry: () => {
const hour = new Date().getHours()
return hour >= 0 && hour <= 6
}
retry: (_, error) => {
return error.isPremiumEndpoint
}
retry: () => {
return navigator.onLine
}
Агрессивный retry способен ухудшить интерфейс.
Проблемы:
Важно понимать: query может находиться в error state, но retry всё ещё продолжается.
Полезные свойства:
const {
error,
failureCount,
failureReason,
isError,
} = useQuery(...)
Показывает число failed attempts.
if (failureCount > 0) {
return <RetryInfo />
}
Содержит последнюю ошибку retry.
console.log(failureReason)
Полезно для debugging.
Иногда интерфейс показывает:
Повторное подключение...
Попытка 2 из 5
Пример:
const query = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
retry: 5,
})
if (query.failureCount > 0) {
return (
<div>
Retry attempt: {query.failureCount}
</div>
)
}
При использовании Suspense retry становится особенно важным.
useSuspenseQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
})
Suspense не показывает ошибку мгновенно — query сначала проходит через retry pipeline.
После исчерпания retry ошибка может попасть в Error Boundary.
throwOnError: true
Комбинация:
retry: 3
throwOnError: true
означает:
Background refetch также использует retry.
Например:
refetchInterval: 30000
Если background refetch падает:
Это один из ключевых механизмов resilience в TanStack Query.
Опасная ситуация в distributed systems.
Сценарий:
Методы защиты:
Иногда retry нужно временно отключать.
Пример идеи:
let serverUnavailable = false
retry: (count, error) => {
if (serverUnavailable) {
return false
}
if (error.status === 503) {
serverUnavailable = true
setTimeout(() => {
serverUnavailable = false
}, 60000)
}
return count < 3
}
Подход предотвращает каскадные сбои.
Наиболее практичная production-конфигурация:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: (count, error) => {
if (error.status >= 400 && error.status < 500) {
return false
}
return count < 3
},
retryDelay: (attempt) => {
const delay = 1000 * 2 ** attempt
const jitter = Math.random() * 1000
return Math.min(delay + jitter, 30000)
},
},
},
})
Особенности: