Персистентные мутации

В TanStack Query мутации представляют собой операции изменения серверного состояния: создание, обновление или удаление данных. В отличие от запросов, мутации не просто кешируются, а проходят через жизненный цикл статусов: idle, pending, success, error.

Персистентные мутации — это механизм сохранения состояния незавершённых или частично завершённых мутаций между сессиями приложения. Это включает восстановление очереди мутаций после перезагрузки страницы, восстановления соединения или повторного открытия приложения.

Ключевая цель — обеспечить непрерывность операций записи в условиях нестабильного окружения (offline-first, mobile web, слабое соединение).


Когда требуется сохранение мутаций

Сохранение мутаций становится критически важным в сценариях:

  • офлайн-режим с последующей синхронизацией
  • нестабильные мобильные сети
  • долгие операции, прерываемые перезагрузкой страницы
  • очередь операций пользователя (например, CRUD в админ-панели)
  • гарантированная доставка изменений при временной недоступности API

В таких условиях отсутствие персистенции приводит к потере пользовательских действий и рассинхронизации клиентского состояния с сервером.


Архитектура MutationCache

В TanStack Query мутации хранятся в MutationCache. Каждая мутация описывается объектом, содержащим:

  • mutationKey
  • state (status, variables, data, error)
  • meta
  • функции выполнения (mutationFn)
  • контекстные данные (context)

Внутренне MutationCache управляет:

  • регистрацией новых мутаций
  • обновлением состояния
  • подписками на изменения
  • удалением завершённых или устаревших записей

Для персистенции важно, что именно MutationCache является источником истины для всех активных мутаций.


Сериализация состояния мутаций

Персистентность невозможна без сериализации. Однако мутации сложнее запросов, так как содержат:

  • функции (mutationFn)
  • не сериализуемые объекты (Promise, Error)
  • контекст выполнения

Поэтому сохраняется только безопасная часть состояния:

  • mutationKey
  • state.status
  • state.data
  • state.error (частично сериализуемый)
  • variables
  • submittedAt
  • meta

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


Persist Client и интеграция с MutationCache

Персистентность реализуется через persistQueryClient из @tanstack/query-persist-client-core.

Базовая схема:

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

const queryClient = new QueryClient({
  defaultOptions: {
    mutations: {
      retry: 3
    }
  }
})

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

persistQueryClient({
  queryClient,
  persister
})

По умолчанию сохраняются query cache и mutation cache, если они не исключены настройками.


Восстановление мутаций после перезагрузки

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

  1. загружается сериализованный cache
  2. восстанавливается MutationCache
  3. пересоздаются объекты мутаций
  4. выполняется проверка статуса (pending, paused, error)

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

  • автоматический retry pending-мутаций
  • ручная синхронизация
  • отложенное выполнение при восстановлении сети

Пример ручной обработки:

queryClient.resumePausedMutations()

Очередь мутаций и paused-состояние

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

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

  • мутация сохранена
  • выполнение отложено
  • она будет возобновлена при вызове resumePausedMutations

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

  1. пользователь отправляет форму
  2. сеть недоступна
  3. мутация переходит в paused
  4. состояние сохраняется в persister
  5. после восстановления сети выполняется повтор

Дедупликация и конфликты при восстановлении

При восстановлении мутаций возникает проблема дубликатов:

  • пользователь мог повторно выполнить действие
  • сервер уже применил изменения
  • состояние клиента устарело

Для решения используются:

mutationKey

Позволяет группировать одинаковые операции:

useMutation({
  mutationKey: ['todo', 'update'],
  mutationFn: updateTodo
})

idempotency keys

На уровне API:

  • уникальные ключи операций
  • предотвращение повторного применения

optimistic reconciliation

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

Offline-first стратегия

Персистентные мутации наиболее полезны в offline-first архитектуре.

Типичный поток:

  1. мутация создаётся локально
  2. сохраняется в MutationCache
  3. persister записывает её в storage
  4. при offline состояние становится paused
  5. при восстановлении сети выполняется replay очереди

Дополнительно используется:

  • onlineManager
  • события window.addEventListener('online')
  • ручной триггер resumePausedMutations

Retry-механизм после восстановления

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

const queryClient = new QueryClient({
  defaultOptions: {
    mutations: {
      retry: (failureCount, error) => {
        if (error?.status === 401) return false
        return failureCount < 3
      }
    }
  }
})

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


Инвалидация запросов после мутаций

После успешного восстановления и выполнения мутации часто требуется обновить query cache:

queryClient.invalidateQueries({ queryKey: ['todos'] })

В offline-first сценариях инвалидация может быть отложенной до завершения очереди мутаций.


Ограничения персистентных мутаций

Несмотря на гибкость, механизм имеет ограничения:

  • функции мутаций не сериализуются
  • сложные контексты (File, Blob, Stream) теряются
  • возможны конфликты данных при долгом офлайне
  • порядок выполнения не всегда гарантирован при параллельных мутациях
  • необходимо вручную управлять бизнес-логикой восстановления

Управление жизненным циклом восстановленных мутаций

После гидратации важно учитывать, что каждая мутация может находиться в одном из состояний:

  • idle — не выполнялась
  • pending — выполняется
  • paused — ожидает сети
  • error — завершилась ошибкой
  • success — выполнена

Контроль осуществляется через:

queryClient.getMutationCache().getAll()

и дальнейшую фильтрацию по состоянию.


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

Для стабильной работы обычно вводится слой оркестрации:

  • очередь мутаций
  • приоритеты операций
  • блокировка конфликтующих действий
  • контроль повторов
  • логирование состояния восстановления

Часто используется комбинация:

  • MutationCache
  • persistQueryClient
  • onlineManager
  • кастомная очередь задач

Контроль восстановления при старте приложения

Типичный порядок инициализации:

  1. создание QueryClient
  2. подключение persister
  3. гидратация cache
  4. восстановление paused mutations
  5. запуск приложения
await persistQueryClient({
  queryClient,
  persister
})

queryClient.resumePausedMutations()

Сценарии сложной синхронизации

В реальных приложениях персистентные мутации применяются для:

  • CRM систем с офлайн-редактированием
  • полевых мобильных приложений
  • редакторов контента
  • систем с очередями задач
  • e-commerce корзин с отложенными операциями

В таких системах мутации становятся не просто HTTP-запросами, а элементами локальной очереди событий.


Контроль целостности данных

Для предотвращения рассинхронизации используется:

  • версионирование данных
  • optimistic updates с rollback
  • серверная валидация состояния
  • повторная синхронизация после восстановления

Особое внимание уделяется тому, чтобы восстановленные мутации не перезаписывали более свежие серверные данные.


Поведение при конфликтующих мутациях

Если несколько мутаций изменяют один ресурс:

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

TanStack Query не навязывает стратегию, оставляя её на уровне архитектуры приложения.