Параметры refetchInterval и refetchOnWindowFocus

В приложениях интерфейс часто должен отображать максимально актуальные данные:

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

Постоянный ручной вызов refetch() создаёт лишний код и усложняет поддержку. В TanStack Query для подобных задач предусмотрены автоматические механизмы повторного получения данных.

Два наиболее важных параметра:

  • refetchInterval
  • refetchOnWindowFocus

Они отвечают за разные сценарии обновления:

Параметр Назначение
refetchInterval Периодический опрос сервера через заданный интервал
refetchOnWindowFocus Обновление данных при возвращении пользователя к вкладке

Параметр refetchInterval

Общий принцип работы

refetchInterval запускает автоматический повторный запрос через указанный промежуток времени.

Пример:

useQuery({
    queryKey: ['stats'],
    queryFn: fetchStats,
    refetchInterval: 5000
})

В данном случае:

  • первый запрос выполняется сразу;
  • затем TanStack Query будет автоматически выполнять повторный запрос каждые 5 секунд.

Как работает интервал обновления

Механизм выглядит следующим образом:

  1. Выполняется запрос.
  2. Данные попадают в кеш.
  3. Запускается таймер.
  4. Через указанный интервал выполняется новый запрос.
  5. Кеш обновляется.
  6. Компоненты получают новые данные.

Значение в миллисекундах

Интервал задаётся в миллисекундах.

Значение Интервал
1000 1 секунда
5000 5 секунд
30000 30 секунд
60000 1 минута

Пример:

refetchInterval: 10000

Запрос будет повторяться каждые 10 секунд.


Использование с useQuery

Базовый пример:

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

function Notifications() {
    const query = useQuery({
        queryKey: ['notifications'],
        queryFn: async () => {
            const response = await fetch('/api/notifications')

            if (!response.ok) {
                throw new Error('Ошибка загрузки')
            }

            return response.json()
        },
        refetchInterval: 5000
    })

    if (query.isLoading) {
        return <div>Загрузка...</div>
    }

    if (query.isError) {
        return <div>Ошибка</div>
    }

    return (
        <ul>
            {query.data.map(item => (
                <li key={item.id}>
                    {item.text}
                </li>
            ))}
        </ul>
    )
}

Поведение при скрытой вкладке

По умолчанию TanStack Query отключает интервальные запросы, если вкладка браузера неактивна.

Это важная оптимизация:

  • уменьшается нагрузка на сервер;
  • снижается потребление батареи;
  • уменьшается сетевой трафик.

Параметр refetchIntervalInBackground

Если необходимо продолжать обновление даже в фоне, используется:

refetchIntervalInBackground: true

Пример:

useQuery({
    queryKey: ['prices'],
    queryFn: fetchPrices,
    refetchInterval: 1000,
    refetchIntervalInBackground: true
})

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


Когда нужен фоновый polling

Подходящие сценарии:

  • торговые терминалы;
  • системы мониторинга;
  • панели администрирования;
  • онлайн-метрики;
  • игровые серверы;
  • live-dashboard.

Когда не стоит использовать фоновые обновления

Неудачные сценарии:

  • редко изменяемые данные;
  • тяжёлые SQL-запросы;
  • мобильные приложения с ограниченной батареей;
  • большие JSON-ответы;
  • высоконагруженные API.

Интервальный polling и staleTime

Очень важно понимать взаимодействие refetchInterval и staleTime.

Пример:

useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
    staleTime: 60000,
    refetchInterval: 5000
})

Несмотря на staleTime: 60000, запрос всё равно будет выполняться каждые 5 секунд.

Причина:

  • staleTime управляет «устареванием» кеша;
  • refetchInterval запускает запрос принудительно по таймеру.

refetchInterval не зависит от freshness-статуса данных.


Динамический интервал обновления

TanStack Query позволяет задавать функцию вместо числа.

Пример:

useQuery({
    queryKey: ['task'],
    queryFn: fetchTask,
    refetchInterval: (data) => {
        if (data?.status === 'finished') {
            return false
        }

        return 2000
    }
})

Особенности:

  • функция получает текущие данные;
  • можно менять интервал динамически;
  • false полностью отключает polling.

Polling до завершения операции

Очень распространённый сценарий:

  1. сервер запускает задачу;
  2. клиент получает taskId;
  3. клиент периодически проверяет статус;
  4. polling останавливается после завершения.

Пример:

useQuery({
    queryKey: ['task-status', taskId],
    queryFn: () => fetchTaskStatus(taskId),
    refetchInterval: (data) => {
        if (data?.completed) {
            return false
        }

        return 3000
    }
})

Условное включение polling

Polling часто требуется только в определённых условиях.

Пример:

useQuery({
    queryKey: ['messages'],
    queryFn: fetchMessages,
    enabled: isAuthorized,
    refetchInterval: isChatOpen ? 2000 : false
})

Здесь:

  • если чат закрыт — polling отключён;
  • если чат открыт — запросы идут каждые 2 секунды.

Влияние на производительность

Слишком маленький интервал способен создать серьёзную нагрузку.

Проблемы:

  • постоянные HTTP-запросы;
  • перегрузка API;
  • лишние рендеры;
  • рост потребления памяти;
  • повышенная нагрузка на мобильные устройства.

Практические рекомендации по интервалам

Тип данных Рекомендуемый интервал
Чат 1–3 секунды
Уведомления 5–15 секунд
Метрики 2–10 секунд
Статистика 30–60 секунд
Справочные данные Без polling

Параметр refetchOnWindowFocus

Основная идея

Когда пользователь возвращается к вкладке браузера, данные могут устареть.

TanStack Query умеет автоматически выполнять обновление в момент фокуса окна.

Пример:

useQuery({
    queryKey: ['profile'],
    queryFn: fetchProfile,
    refetchOnWindowFocus: true
})

Поведение по умолчанию

По умолчанию параметр уже включён:

refetchOnWindowFocus: true

Это означает:

  • пользователь переключился на другую вкладку;
  • прошло некоторое время;
  • пользователь вернулся обратно;
  • TanStack Query повторно запрашивает данные.

Когда это особенно полезно

Типичные сценарии:

  • список заказов;
  • состояние баланса;
  • уведомления;
  • сообщения;
  • административные панели;
  • совместная работа нескольких пользователей.

Как TanStack Query определяет фокус

Библиотека отслеживает:

  • visibilitychange
  • focus

После возвращения вкладки в активное состояние запускается проверка необходимости refetch.


Refetch выполняется не всегда

Важно понимать:

refetchOnWindowFocus обычно срабатывает только для stale-данных.

Пример:

useQuery({
    queryKey: ['settings'],
    queryFn: fetchSettings,
    staleTime: 60000,
    refetchOnWindowFocus: true
})

Если пользователь вернулся к вкладке через 10 секунд, повторного запроса не будет.

Причина:

  • данные ещё считаются fresh.

Принудительное обновление при фокусе

Можно заставить запрос выполняться всегда:

refetchOnWindowFocus: 'always'

Пример:

useQuery({
    queryKey: ['feed'],
    queryFn: fetchFeed,
    refetchOnWindowFocus: 'always'
})

Теперь refetch будет происходить при каждом возвращении к вкладке.


Полное отключение

refetchOnWindowFocus: false

Пример:

useQuery({
    queryKey: ['countries'],
    queryFn: fetchCountries,
    refetchOnWindowFocus: false
})

Когда отключение оправдано

Подходящие случаи:

  • статические данные;
  • редко изменяемая информация;
  • тяжёлые API;
  • большие таблицы;
  • справочники;
  • конфигурационные данные.

Совместное использование refetchInterval и refetchOnWindowFocus

Оба механизма могут работать одновременно.

Пример:

useQuery({
    queryKey: ['dashboard'],
    queryFn: fetchDashboard,
    refetchInterval: 10000,
    refetchOnWindowFocus: true
})

Поведение:

  • каждые 10 секунд выполняется polling;
  • при возврате к вкладке выполняется дополнительная проверка актуальности.

Возможные проблемы при совместном использовании

Если интервал слишком маленький:

refetchInterval: 1000

то refetchOnWindowFocus часто становится бессмысленным, поскольку данные и так почти всегда свежие.


Отличие polling от focus refetch

Механизм Когда срабатывает
refetchInterval По таймеру
refetchOnWindowFocus При возвращении к вкладке

Поведение при ошибках

Если запрос завершился ошибкой:

  • polling продолжит работать;
  • при фокусе окна тоже может произойти повторная попытка.

Пример:

useQuery({
    queryKey: ['server'],
    queryFn: fetchServer,
    refetchInterval: 5000,
    retry: 3
})

Влияние на UX

Автоматические refetch-механизмы позволяют:

  • поддерживать интерфейс актуальным;
  • уменьшать количество ручных обновлений;
  • скрывать сетевую сложность;
  • улучшать ощущение «живого» интерфейса.

Но чрезмерное количество запросов приводит к обратному эффекту:

  • интерфейс начинает постоянно перерисовываться;
  • появляются скачки контента;
  • растёт нагрузка на API.

Рекомендуемые стратегии

Для dashboard

refetchInterval: 5000
refetchOnWindowFocus: true

Для справочников

staleTime: Infinity
refetchOnWindowFocus: false

Для чата

refetchInterval: 2000
refetchIntervalInBackground: true

Для профиля пользователя

staleTime: 300000
refetchOnWindowFocus: true

Частая ошибка: слишком агрессивный polling

Плохой пример:

refetchInterval: 100

Такой код способен:

  • перегрузить сервер;
  • вызвать блокировки API;
  • резко увеличить сетевую активность;
  • ухудшить производительность приложения.

Частая ошибка: polling для неизменяемых данных

Ненужный пример:

useQuery({
    queryKey: ['countries'],
    queryFn: fetchCountries,
    refetchInterval: 5000
})

Список стран почти не меняется, поэтому polling здесь бессмысленен.


Частая ошибка: забытый polling

Иногда polling остаётся после отладки:

refetchInterval: 1000

В production это может привести к тысячам лишних запросов в минуту.


Глобальная настройка

Параметры можно задавать глобально через QueryClient.

Пример:

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

Эти настройки применятся ко всем запросам по умолчанию.


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

Локальные настройки имеют приоритет:

useQuery({
    queryKey: ['chat'],
    queryFn: fetchChat,
    refetchInterval: 2000
})

Даже если глобально установлен другой интервал, конкретный запрос будет использовать локальное значение.