Мутации в TanStack Query представляют собой операции изменения серверного состояния: создание, обновление и удаление данных. В отличие от запросов, мутации не кешируются как источник данных, но фиксируются в отдельном слое состояния — mutation cache, который отслеживает жизненный цикл каждой операции: idle → pending → success → error.
Каждая мутация изолирована, однако библиотека не накладывает ограничений на их параллельное выполнение. Несколько мутаций могут быть запущены одновременно, независимо друг от друга, что формирует основу для параллельных сценариев.
Параллельные мутации — это одновременный запуск нескольких независимых операций изменения данных. Такой подход используется при необходимости обновить несколько сущностей или коллекций без ожидания завершения предыдущей операции.
Ключевая особенность заключается в том, что TanStack Query не
сериализует мутации автоматически. Каждая mutate или
mutateAsync вызывает отдельный execution context.
const mutationA = useMutation({
mutationFn: (data) => api.updateUser(data),
});
const mutationB = useMutation({
mutationFn: (data) => api.updateProfile(data),
});
mutationA.mutate({ name: "Alex" });
mutationB.mutate({ theme: "dark" });
Обе операции выполняются независимо и могут завершиться в любом порядке.
Каждый вызов useMutation создаёт отдельный экземпляр
состояния. Это означает:
Такое разделение позволяет безопасно запускать операции параллельно без конфликтов состояния.
const updateUser = useMutation({
mutationFn: api.updateUser,
});
const updateSettings = useMutation({
mutationFn: api.updateSettings,
});
updateUser.mutate({ id: 1, name: "John" });
updateSettings.mutate({ theme: "dark" });
Каждая мутация управляет своим состоянием, не блокируя другую.
Различие между mutate и mutateAsync
становится критичным при параллельном выполнении.
mutate — fire-and-forget подходmutateAsync — возвращает Promise и позволяет управлять
порядком выполненияawait Promise.all([
mutationA.mutateAsync({ name: "Alex" }),
mutationB.mutateAsync({ theme: "dark" }),
]);
Использование Promise.all позволяет синхронизировать
завершение группы мутаций, сохраняя их параллельное выполнение.
Параллельные операции часто требуют согласованного завершения. Это особенно актуально при обновлении связанных сущностей.
const result = await Promise.all([
api.updateUser({ id: 1, name: "Alex" }),
api.updateProfile({ id: 1, theme: "dark" }),
api.updatePreferences({ id: 1, lang: "ru" }),
]);
В рамках TanStack Query такой подход часто используется внутри
mutationFn или в orchestration-слое над мутациями.
Mutation cache хранит все активные мутации независимо друг от друга. При параллельном запуске формируется несколько записей с разными идентификаторами.
Каждая запись содержит:
Параллельное выполнение приводит к одновременному существованию
нескольких pending состояний.
const mutation = useMutation({
mutationFn: api.saveItem,
mutationKey: ["save-item"],
});
Если вызвать mutate несколько раз подряд, TanStack Query
создаст отдельные инстансы операций внутри cache.
Параллельные мутации могут приводить к race conditions при работе с shared state.
Типичные сценарии конфликтов:
mutationA.mutate({ id: 1, name: "A" });
mutationB.mutate({ id: 1, name: "B" });
Результат зависит от порядка завершения запросов, а не от порядка вызова.
Оптимистические обновления усиливают риск конфликтов при параллельных мутациях. Каждый mutation может локально модифицировать кеш до завершения запроса.
const mutation = useMutation({
mutationFn: api.updateItem,
onMutate: async (newData) => {
await queryClient.cancelQueries({ queryKey: ["items"] });
const prev = queryClient.getQueryData(["items"]);
queryClient.setQueryData(["items"], (old) =>
old.map((item) =>
item.id === newData.id ? { ...item, ...newData } : item
)
);
return { prev };
},
onError: (err, newData, context) => {
queryClient.setQueryData(["items"], context.prev);
},
});
При параллельных мутациях каждая из них может откатить или перезаписать изменения другой, если не учитывать изоляцию состояния.
Одновременные мутации часто приводят к множественным вызовам
invalidateQueries, что может вызвать избыточные
refetch-запросы.
mutationA.mutateAsync(dataA).then(() => {
queryClient.invalidateQueries({ queryKey: ["items"] });
});
mutationB.mutateAsync(dataB).then(() => {
queryClient.invalidateQueries({ queryKey: ["items"] });
});
В результате один и тот же query может инвалидироваться несколько раз подряд.
Оптимизация заключается в координации инвалидирования:
await Promise.all([
mutationA.mutateAsync(dataA),
mutationB.mutateAsync(dataB),
]);
queryClient.invalidateQueries({ queryKey: ["items"] });
При параллельных мутациях возможен сценарий partial success, когда часть операций завершается успешно, а часть — с ошибкой.
const results = await Promise.allSettled([
api.updateUser(data),
api.updateProfile(data),
api.updateSettings(data),
]);
TanStack Query не объединяет такие состояния автоматически, поэтому обработка результата выполняется на уровне orchestration-логики.
Параллельные мутации противопоставляются очередям выполнения.
Очередь:
Параллельность:
TanStack Query не предоставляет встроенной очереди мутаций, поэтому последовательность реализуется вручную:
await mutationA.mutateAsync(dataA);
await mutationB.mutateAsync(dataB);
При одновременном запуске нескольких мутаций важно учитывать состояние UI-компонентов:
isPending могут быть активны
одновременноconst isLoading =
mutationA.isPending || mutationB.isPending || mutationC.isPending;
Агрегация состояния становится частью архитектуры при массовых обновлениях.
После параллельных мутаций чаще всего требуется синхронизация query cache.
await Promise.all([
mutationA.mutateAsync(dataA),
mutationB.mutateAsync(dataB),
]);
queryClient.setQueryData(["items"], (old) =>
old.map(applyLocalChanges)
);
Локальное обновление кеша может уменьшить количество refetch-запросов и стабилизировать UI после серии параллельных операций.
Каждая мутация имеет собственный механизм retry. При параллельном запуске это означает независимые retry-циклы.
useMutation({
mutationFn: api.save,
retry: 3,
});
Если одновременно запущено несколько мутаций, каждая может находиться в собственном retry-loop, что увеличивает нагрузку на сервер при массовых сбоях.
Хотя mutationKey не влияет на кеширование как в queries, он позволяет группировать мутации для аналитики и контроля.
useMutation({
mutationKey: ["user", "bulk-update"],
mutationFn: api.bulkUpdate,
});
В параллельных сценариях mutationKey помогает различать типы операций и строить поверх них контрольные механизмы.
На практике используются несколько устойчивых моделей:
Выбор модели зависит от стоимости запросов, консистентности данных и нагрузки на backend.