Очереди мутаций в TanStack Query формируются не как отдельная
встроенная структура с явным API уровня “queue”, а как комбинация
механизмов mutationCache, статуса мутаций, ключей
(mutationKey) и пользовательских паттернов сериализации. На
практике это слой управления конкурентным выполнением побочных эффектов,
где важны порядок, идемпотентность и контроль состояния между запросами
на изменение данных.
Мутация в TanStack Query представляет собой асинхронную операцию
изменения серверного состояния: создание, обновление или удаление
данных. В отличие от запросов (queries), мутации не
кэшируются как результат, но их жизненный цикл отслеживается через
mutationCache.
Каждая мутация проходит стадии:
Внутри библиотеки каждая мутация регистрируется в глобальном
MutationCache, что позволяет отслеживать их состояние,
подписываться на изменения и реализовывать сложные сценарии управления
порядком выполнения.
Очередь мутаций требуется там, где параллельное выполнение приводит к неконсистентности:
Типичный пример — редактирование списка задач, где пользователь быстро меняет порядок элементов или статус нескольких сущностей подряд.
TanStack Query по умолчанию не сериализует мутации. Несколько вызовов
mutate выполняются параллельно:
const mutation = useMutation({
mutationFn: updateTodo,
})
mutation.mutate({ id: 1, title: 'A' })
mutation.mutate({ id: 1, title: 'B' })
Результат зависит от скорости сети и сервера: второй запрос может завершиться раньше первого, что приводит к перетиранию данных.
Эта проблема и является основной причиной построения очередей.
Самый простой механизм очереди — цепочка
await mutateAsync.
const mutation = useMutation({
mutationFn: updateTodo,
})
async function runQueue() {
await mutation.mutateAsync({ id: 1, title: 'A' })
await mutation.mutateAsync({ id: 1, title: 'B' })
await mutation.mutateAsync({ id: 1, title: 'C' })
}
Это линейная очередь, где каждая операция ожидает завершения предыдущей.
Недостаток — отсутствие централизованного управления и невозможность динамического добавления задач в очередь без внешнего контейнера.
mutationKey позволяет группировать мутации по
логическому признаку. Это основа для построения очередей на уровне
сущностей.
useMutation({
mutationKey: ['todo', 'upd ate', todoId],
mutationFn: updateTodo,
})
Сам по себе ключ не создаёт очередь, но даёт возможность:
MutationCacheЧерез mutationCache можно получать активные мутации:
queryClient.getMutationCache().findAll({
mutationKey: ['todo', 'update', todoId],
})
Практический подход — создание очереди поверх
mutateAsync.
class MutationQueue {
constructor() {
this.queue = []
this.running = false
}
enqueue(task) {
return new Promise((resolve, reject) => {
this.queue.push({ task, resolve, reject })
this.process()
})
}
async process() {
if (this.running) return
this.running = true
while (this.queue.length) {
const { task, resolve, reject } = this.queue.shift()
try {
const result = await task()
resolve(result)
} catch (e) {
reject(e)
}
}
this.running = false
}
}
Использование с TanStack Query:
const queue = new MutationQueue()
const mutation = useMutation({
mutationFn: updateTodo,
})
function updateTodoQueued(data) {
return queue.enqueue(() => mutation.mutateAsync(data))
}
Такая очередь гарантирует последовательное выполнение мутаций.
Наиболее распространённый сценарий — очередь на конкретный ресурс
(например, один todoId), при этом другие ресурсы могут
изменяться параллельно.
const queues = new Map()
function getQueue(id) {
if (!queues.has(id)) {
queues.se t(id, new MutationQueue())
}
return queues.get(id)
}
function updateTodoQueued(id, data, mutation) {
const queue = getQueue(id)
return queue.enqueue(() =>
mutation.mutateAsync({ id, ...data })
)
}
Такой подход устраняет гонки внутри одного объекта, но сохраняет параллельность между разными сущностями.
Очереди мутаций часто сочетаются с оптимистическими обновлениями
через onMutate.
useMutation({
mutationFn: updateTodo,
onMutate: async (newData) => {
await queryClient.cancelQueries(['todo', newData.id])
const previous = queryClient.getQueryData(['todo', newData.id])
queryClient.setQueryData(['todo', newData.id], old => ({
...old,
...newData,
}))
return { previous }
},
onError: (err, newData, context) => {
queryClient.setQueryData(
['todo', newData.id],
context.previous
)
},
})
При использовании очереди важно, чтобы rollback не нарушал порядок выполнения. Поэтому откаты должны применяться строго к конкретной мутации, а не ко всей очереди.
TanStack Query автоматически поддерживает retry для мутаций. В контексте очередей это создаёт дополнительный уровень сложности: повторная попытка может блокировать очередь.
useMutation({
mutationFn: updateTodo,
retry: 3,
retryDelay: 1000,
})
Если очередь последовательная, retry фактически “удлиняет” выполнение всей цепочки. Это может быть желаемым поведением (строгая консистентность), либо проблемой (заморозка очереди).
Для управления этим используют:
В связке с networkMode: 'offlineFirst' мутации могут
откладываться до восстановления сети. Это формирует естественную очередь
синхронизации.
useMutation({
mutationFn: updateTodo,
networkMode: 'offlineFirst',
})
При офлайн-режиме TanStack Query сохраняет мутации и выполняет их при восстановлении соединения. В таких сценариях порядок выполнения становится критичным, особенно при зависимых изменениях данных.
При использовании persistence (persistQueryClient) можно
сохранять не только queries, но и состояние мутаций. Это позволяет
восстанавливать очередь после перезапуска приложения.
Основная сложность:
Пример концепта:
persistQueryClient({
queryClient,
persister,
})
Восстановление очереди строится через:
mutateAsyncДля сложных приложений применяется централизованная очередь.
class GlobalMutationQueue {
constructor(queryClient) {
this.queryClient = queryClient
this.queue = []
this.running = false
}
add(mutationFn, variables) {
return new Promise((resolve, reject) => {
this.queue.push({ mutationFn, variables, resolve, reject })
this.run()
})
}
async run() {
if (this.running) return
this.running = true
while (this.queue.length) {
const item = this.queue.shift()
try {
const result = await item.mutationFn(item.variables)
item.resolve(result)
} catch (e) {
item.reject(e)
}
}
this.running = false
}
}
Интеграция:
const queue = new GlobalMutationQueue(queryClient)
const mutation = useMutation({
mutationFn: updateTodo,
})
function update(data) {
return queue.add(mutation.mutateAsync, data)
}
При очередях мутаций критически важна стратегия инвалидирования кэша:
Типичный паттерн:
queryClient.invalidateQueriesОчередь и батчинг часто путаются, но имеют разные цели:
Очередь:
Батчинг:
Очередь может содержать батчи, но батч не гарантирует порядок между элементами.
Используются три основных подхода:
Пер-ресурсные очереди Изоляция по entityId
Глобальная очередь Полная сериализация мутаций
Гибридная модель Очереди по ключу + глобальный лимитер параллелизма
Гибрид часто реализуется как:
MutationCache позволяет отслеживать активные
мутации:
queryClient.getMutationCache().getAll()
Это используется для:
В реальных приложениях очередь мутаций обычно включает:
Такой слой становится промежуточным “mutation orchestration layer” поверх TanStack Query, который превращает библиотеку из простого клиента серверного состояния в управляемую систему выполнения операций изменения данных.