TanStack Query использует поведение фокуса окна как один из ключевых сигналов для управления актуальностью данных. Смена активного окна браузера, возврат пользователя на вкладку или повторное получение фокуса могут автоматически запускать повторные запросы и обновление кэша.
Фокус окна рассматривается как триггер «потенциальной устарелости данных». Логика основана на предположении, что пользователь мог долго отсутствовать, а данные за это время могли измениться.
Основная настройка, отвечающая за поведение при возврате фокуса, —
refetchOnWindowFocus.
import { QueryClient } from '@tanstack/react-query'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: true,
},
},
})
При значении true происходит автоматический рефетч
активных (mounted) и устаревших (stale) запросов при возвращении фокуса
окна.
Поведение зависит от нескольких условий:
Если staleTime задан как бесконечный, автоматический
рефетч не произойдёт:
{
staleTime: Infinity,
refetchOnWindowFocus: true // не даст эффекта
}
Настройка может задаваться на трёх уровнях:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: true,
},
},
})
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
refetchOnWindowFocus: false,
})
refetchOnWindowFocus: (query) => {
return query.state.dataUpdatedAt < Date.now() - 60000
}
Функциональная форма используется для динамического контроля поведения в зависимости от состояния кэша.
Внутри библиотеки используется focusManager, который
централизует обработку событий фокуса окна.
Он управляет двумя ключевыми аспектами:
import { focusManager } from '@tanstack/react-query'
focusManager.setEventListener((handleFocus) => {
window.addEventListener('focus', handleFocus)
window.addEventListener('visibilitychange', handleFocus)
return () => {
window.removeEventListener('focus', handleFocus)
window.removeEventListener('visibilitychange', handleFocus)
}
})
window.focusvisibilitychangepageshow (для восстановления из bfcache)Использование нескольких событий необходимо для покрытия разных браузерных сценариев, включая восстановление вкладки из кеша истории.
focus срабатывает при активизации окна браузера, но не
всегда корректно отражает реальное «возвращение пользователя».
visibilitychange отслеживает изменение видимости
вкладки:
document.hidden === true — вкладка скрытаdocument.hidden === false — вкладка активнаTanStack Query комбинирует оба сигнала, чтобы избежать пропусков в браузерных реализациях.
Фокус окна часто работает совместно с onlineManager,
который отслеживает сетевую доступность.
import { onlineManager } from '@tanstack/react-query'
onlineManager.setEventListener(setOnline => {
window.addEventListener('online', () => setOnline(true))
window.addEventListener('offline', () => setOnline(false))
return () => {
window.removeEventListener('online', () => setOnline(true))
window.removeEventListener('offline', () => setOnline(false))
}
})
При возвращении фокуса может происходить рефетч, но только если сеть доступна. Таким образом формируется двойной фильтр:
При наступлении события фокуса QueryClient проверяет:
refetchOnWindowFocusenabled, retry,
networkModeТолько после прохождения всех условий инициируется повторный запрос.
staleTime напрямую влияет на реакцию фокуса.
useQuery({
queryKey: ['profile'],
queryFn: fetchProfile,
staleTime: 1000 * 60 * 5,
})
При таком значении данные считаются свежими 5 минут, и возврат фокуса в этот период не вызовет рефетч.
С точки зрения внутренней логики:
now - dataUpdatedAt < staleTime → данные свежиеФункциональная форма позволяет учитывать сложные условия:
refetchOnWindowFocus: (query) => {
const lastUpdate = query.state.dataUpdatedAt
const age = Date.now() - lastUpdate
return age > 30_000
}
Применяется в случаях:
Фокус можно эмулировать вручную:
import { focusManager } from '@tanstack/react-query'
focusManager.setFocused(true)
Это полезно для:
Полное отключение реакции на фокус:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
})
Или выборочно:
useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
refetchOnWindowFocus: false,
})
В таком режиме данные обновляются только через:
В React Native нет window, поэтому используется
абстракция focusManager.
Типичная реализация:
import { AppState } from 'react-native'
import { focusManager } from '@tanstack/react-query'
focusManager.setEventListener(handleFocus => {
const subscription = AppState.addEventListener('change', state => {
handleFocus(state === 'active')
})
return () => subscription.remove()
})
Здесь фокус окна заменяется на состояние приложения:
active → приложение на переднем планеbackground → приложение свернутоПри серверном рендеринге фокус отсутствует, поэтому:
Важно учитывать, что при гидратации возможен первый автоматический рефетч, если окно уже в фокусе.
При резких переключениях вкладок возможны множественные события focus/visibilitychange. TanStack Query включает защиту от повторного запуска:
Дополнительно можно стабилизировать поведение через внешние механизмы:
refetchOnWindowFocus: (query) => {
const now = Date.now()
const last = query.state.dataUpdatedAt
return now - last > 10000
}
В режимах вроде offlineFirst или always
поведение фокуса может отличаться:
online режим требует сети для рефетчаalways может инициировать запрос даже при оффлайн, но с
отложенной очередьюofflineFirst приоритетно использует кешФокус сам по себе не гарантирует выполнение запроса — он только инициирует попытку.
Фокусное обновление особенно эффективно в следующих моделях:
В таких системах фокус работает как мягкий триггер синхронизации без постоянного polling.
Если включён интервал обновления:
useQuery({
queryKey: ['notifications'],
queryFn: fetchNotifications,
refetchInterval: 30000,
refetchOnWindowFocus: true,
})
поведение становится комбинированным:
При возврате фокуса запрос может быть выполнен немедленно, даже если интервал недавно сработал, если данные стали stale.
При открытии одного приложения в нескольких вкладках:
Без дополнительной настройки возможны ситуации:
Полный контроль над логикой фокуса достигается заменой event listener:
focusManager.setEventListener((handleFocus) => {
const onFo cus = () => handleFocus(true)
const onB lur = () => handleFocus(false)
document.addEventListener('visibilitychange', () => {
handleFocus(!document.hidden)
})
return () => {
document.removeEventListener('visibilitychange', onFocus)
document.removeEventListener('visibilitychange', onBlur)
}
})
Такая кастомизация используется в: