Настройка поведения фокуса окна

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

Фокус окна рассматривается как триггер «потенциальной устарелости данных». Логика основана на предположении, что пользователь мог долго отсутствовать, а данные за это время могли измениться.


Базовый механизм: refetchOnWindowFocus

Основная настройка, отвечающая за поведение при возврате фокуса, — refetchOnWindowFocus.

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

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      refetchOnWindowFocus: true,
    },
  },
})

При значении true происходит автоматический рефетч активных (mounted) и устаревших (stale) запросов при возвращении фокуса окна.

Поведение зависит от нескольких условий:

  • запрос должен быть в состоянии stale
  • запрос должен иметь активных подписчиков (компоненты смонтированы)
  • окно должно действительно потерять и вернуть фокус

Если staleTime задан как бесконечный, автоматический рефетч не произойдёт:

{
  staleTime: Infinity,
  refetchOnWindowFocus: true // не даст эффекта
}

Уровни настройки поведения

Настройка может задаваться на трёх уровнях:

Глобальный уровень QueryClient

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

Внутри библиотеки используется focusManager, который централизует обработку событий фокуса окна.

Он управляет двумя ключевыми аспектами:

  • регистрацией обработчиков событий браузера
  • единым API для имитации фокуса (полезно в тестах и SSR)
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.focus
  • visibilitychange
  • иногда pageshow (для восстановления из bfcache)

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


Разница между focus и visibilitychange

focus срабатывает при активизации окна браузера, но не всегда корректно отражает реальное «возвращение пользователя».

visibilitychange отслеживает изменение видимости вкладки:

  • document.hidden === true — вкладка скрыта
  • document.hidden === false — вкладка активна

TanStack Query комбинирует оба сигнала, чтобы избежать пропусков в браузерных реализациях.


OnlineManager и связь с фокусом

Фокус окна часто работает совместно с 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 проверяет:

  1. Есть ли активные наблюдатели query
  2. Является ли query устаревшим
  3. Включён ли refetchOnWindowFocus
  4. Не происходит ли уже запрос
  5. Есть ли ограничения enabled, retry, networkMode

Только после прохождения всех условий инициируется повторный запрос.


Взаимодействие с staleTime

staleTime напрямую влияет на реакцию фокуса.

useQuery({
  queryKey: ['profile'],
  queryFn: fetchProfile,
  staleTime: 1000 * 60 * 5,
})

При таком значении данные считаются свежими 5 минут, и возврат фокуса в этот период не вызовет рефетч.

С точки зрения внутренней логики:

  • now - dataUpdatedAt < staleTime → данные свежие
  • фокус не инициирует повторный запрос

Опция refetchOnWindowFocus как функция

Функциональная форма позволяет учитывать сложные условия:

refetchOnWindowFocus: (query) => {
  const lastUpdate = query.state.dataUpdatedAt
  const age = Date.now() - lastUpdate

  return age > 30_000
}

Применяется в случаях:

  • частично офлайн-ориентированных приложений
  • дорогих API запросов
  • систем с кэшированием на сервере

Кастомизация поведения через focusManager.setFocused

Фокус можно эмулировать вручную:

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

focusManager.setFocused(true)

Это полезно для:

  • тестирования
  • SSR гидратации
  • мобильных оболочек (WebView)

Отключение автоматического поведения

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

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      refetchOnWindowFocus: false,
    },
  },
})

Или выборочно:

useQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts,
  refetchOnWindowFocus: false,
})

В таком режиме данные обновляются только через:

  • ручной invalidate
  • refetch
  • refetchOnReconnect
  • polling (refetchInterval)

Поведение в React Native и нестандартных окружениях

В 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 → приложение свернуто

Взаимодействие с SSR

При серверном рендеринге фокус отсутствует, поэтому:

  • refetchOnWindowFocus не срабатывает
  • состояние считается «неактивным»
  • после гидратации браузер заново устанавливает listeners

Важно учитывать, что при гидратации возможен первый автоматический рефетч, если окно уже в фокусе.


debounce и защита от повторных рефетчей

При резких переключениях вкладок возможны множественные события focus/visibilitychange. TanStack Query включает защиту от повторного запуска:

  • игнорирование дубликатов
  • проверка активного запроса
  • внутренний deduping на уровне queryKey

Дополнительно можно стабилизировать поведение через внешние механизмы:

refetchOnWindowFocus: (query) => {
  const now = Date.now()
  const last = query.state.dataUpdatedAt

  return now - last > 10000
}

Связь с networkMode

В режимах вроде offlineFirst или always поведение фокуса может отличаться:

  • online режим требует сети для рефетча
  • always может инициировать запрос даже при оффлайн, но с отложенной очередью
  • offlineFirst приоритетно использует кеш

Фокус сам по себе не гарантирует выполнение запроса — он только инициирует попытку.


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

Фокусное обновление особенно эффективно в следующих моделях:

  • административные панели с часто меняющимися данными
  • дашборды аналитики
  • чаты и ленты событий
  • CRM-интерфейсы с параллельными изменениями

В таких системах фокус работает как мягкий триггер синхронизации без постоянного polling.


Комбинация с refetchInterval

Если включён интервал обновления:

useQuery({
  queryKey: ['notifications'],
  queryFn: fetchNotifications,
  refetchInterval: 30000,
  refetchOnWindowFocus: true,
})

поведение становится комбинированным:

  • интервал обеспечивает регулярное обновление
  • фокус обеспечивает «догоняющее» обновление после отсутствия пользователя

При возврате фокуса запрос может быть выполнен немедленно, даже если интервал недавно сработал, если данные стали stale.


Поведение при нескольких вкладках

При открытии одного приложения в нескольких вкладках:

  • каждая вкладка имеет свой QueryClient
  • фокус в одной вкладке не влияет напрямую на другие
  • возможна синхронизация через broadcast-channel или custom event bridge

Без дополнительной настройки возможны ситуации:

  • одна вкладка обновила данные
  • другая остаётся со старым кешем до следующего фокуса

Управление низкоуровневым поведением

Полный контроль над логикой фокуса достигается заменой 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)
  }
})

Такая кастомизация используется в:

  • electron-приложениях
  • embedded webviews
  • нестандартных браузерных контейнерах