Стратегии работы офлайн

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

Ключевая идея офлайн-стратегий в TanStack Query заключается в том, что кэш становится основным источником истины на время отсутствия соединения, а синхронизация с сервером откладывается до восстановления сети.


Поведение кэша при отсутствии сети

TanStack Query сохраняет результаты запросов в кэше, привязанном к ключам queryKey. При отсутствии сети поведение приложения определяется комбинацией нескольких параметров:

  • кэшированные данные остаются доступны
  • запросы переходят в состояние error или pending в зависимости от конфигурации retry
  • автоматические повторные попытки могут увеличивать задержку
  • фоновые обновления откладываются до восстановления соединения

Основной механизм устойчивости — использование ранее загруженных данных вместо попыток немедленного повторного запроса.


staleTime и gcTime как основа офлайн-логики

Два параметра играют центральную роль в поведении данных:

staleTime

staleTime определяет период, в течение которого данные считаются актуальными.

При офлайн-сценариях увеличение staleTime приводит к тому, что приложение не пытается сразу обновлять данные при каждом монтировании компонента.

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

В офлайн-режиме это снижает количество бесполезных попыток синхронизации.

gcTime

gcTime (garbage collection time) управляет временем хранения неиспользуемых данных в кэше.

При корректной настройке gcTime можно обеспечить сохранность данных между сессиями, если используется persistence слой.


Persisted cache как основа офлайн-доступа

Для полноценной офлайн-поддержки используется персистентный кэш. TanStack Query позволяет сохранять состояние кэша в внешнее хранилище.

Типичная стратегия включает:

  • сохранение кэша в localStorage (web)
  • использование IndexedDB для больших объемов данных
  • AsyncStorage для React Native

Базовая интеграция выглядит через persistQueryClient:

import { QueryClient } from '@tanstack/react-query'
import { persistQueryClient } from '@tanstack/react-query-persist-client'
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister'

const queryClient = new QueryClient()

const persister = createSyncStoragePersister({
  storage: window.localStorage
})

persistQueryClient({
  queryClient,
  persister
})

При таком подходе данные переживают перезагрузку страницы и отсутствие сети.


Поведение запросов при отсутствии сети

TanStack Query не содержит встроенного детектора сети как обязательного компонента, но его поведение адаптируется к ошибкам fetch.

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

  • запрос завершается с ошибкой network error
  • включается retry логика
  • состояние isError становится true
  • данные остаются из кэша, если enabled keepPreviousData

Конфигурация retry критична:

useQuery({
  queryKey: ['profile'],
  queryFn: fetchProfile,
  retry: (failureCount, error) => {
    if (!navigator.onLine) return false
    return failureCount < 3
  }
})

Отключение retry при офлайн-состоянии предотвращает лишние попытки.


Управление онлайн/офлайн состоянием

Для корректной работы офлайн-режима важно синхронизировать состояние сети с поведением TanStack Query.

Обычно используется браузерное API:

  • window.addEventListener(‘online’)
  • window.addEventListener(‘offline’)

И принудительное управление queryClient:

window.addEventListener('online', () => {
  queryClient.resumePausedMutations()
  queryClient.invalidateQueries()
})

При восстановлении сети выполняются:

  • повтор отправки отложенных мутаций
  • рефетч устаревших данных

Офлайн-мутации и очередь изменений

Наиболее сложный аспект офлайн-архитектуры связан с мутациями данных.

TanStack Query позволяет реализовать очередь изменений через:

  • onMutate
  • onError
  • onSettled
  • rollback optimistic updates

Оптимистичные обновления

useMutation({
  mutationFn: updateTodo,
  onMutate: async (newTodo) => {
    await queryClient.cancelQueries(['todos'])

    const previous = queryClient.getQueryData(['todos'])

    queryClient.setQueryData(['todos'], old =>
      old.map(t => t.id === newTodo.id ? newTodo : t)
    )

    return { previous }
  },
  onError: (err, newTodo, context) => {
    queryClient.setQueryData(['todos'], context.previous)
  }
})

При отсутствии сети изменения могут быть применены локально и откатаны при ошибке.


Очередь мутаций для офлайн-режима

Для полноценной офлайн-работы применяется паттерн очереди:

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

TanStack Query не предоставляет полноценную очередь “из коробки”, но интегрируется с кастомными persistence слоями.

Пример логики:

  • onMutate сохраняет действие в IndexedDB
  • при online событии выполняется обработка очереди
  • после успеха вызывается invalidateQueries

Background refetch после восстановления сети

После перехода в online режим важно не перегружать сервер и интерфейс.

TanStack Query использует:

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

Такое поведение позволяет синхронизировать данные без лишнего трафика.


Hydration состояния кэша

В приложениях SSR или гибридных системах важную роль играет hydration.

Сервер может передать предварительно загруженные данные, а клиент продолжает работу офлайн с уже заполненным кэшем.

import { dehydrate, HydrationBoundary } from '@tanstack/react-query'

const dehydratedState = dehydrate(queryClient)

При восстановлении кэша офлайн-режим получает стартовые данные без необходимости сети.


Сегментация данных для офлайн-режима

Не все данные одинаково подходят для офлайн-доступа. Практика делит их на категории:

  • статические справочники
  • пользовательские данные
  • транзакционные данные
  • временные данные

Для каждой категории задаются разные параметры:

  • staleTime увеличивается для статических данных
  • gcTime увеличивается для справочников
  • retry отключается для транзакционных запросов

Инвалидация и консистентность

При восстановлении сети основная задача — привести локальный кэш в соответствие с сервером.

Используются механизмы:

  • invalidateQueries по ключам
  • refetchQueries для критических данных
  • setQueryData для локальных корректировок
queryClient.invalidateQueries({
  queryKey: ['todos']
})

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


Конфликты данных при синхронизации

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

Основные стратегии:

  • last write wins
  • server authoritative
  • merge strategy на уровне клиента

TanStack Query предоставляет инструменты для реализации любого подхода через onSuccess и onError мутаций, а также через ручное управление кэшем.


Оптимизация повторных попыток

Retry-логика требует особого внимания в офлайн-режиме. Без ограничений она приводит к:

  • блокировке UI
  • лишнему трафику после восстановления сети
  • увеличенной нагрузке на сервер

Оптимальная стратегия:

  • отключение retry при offline
  • экспоненциальная задержка при transient errors
  • ограничение количества попыток

Использование focus и visibility для синхронизации

События видимости вкладки влияют на стратегию обновления данных.

useQuery({
  queryKey: ['messages'],
  queryFn: fetchMessages,
  refetchOnWindowFocus: true
})

При возвращении пользователя в приложение данные синхронизируются автоматически, если сеть доступна.


Архитектура офлайн-first приложения

Типовая архитектура включает:

  • TanStack Query как слой состояния данных
  • persister для сохранения кэша
  • network listener для управления состоянием соединения
  • mutation queue для офлайн-операций
  • background sync после восстановления сети

Такая структура позволяет отделить UI от сетевой логики и обеспечить предсказуемое поведение при любых условиях подключения.