Параллельные мутации

Мутации в 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

Каждый вызов useMutation создаёт отдельный экземпляр состояния. Это означает:

  • отдельный статус загрузки
  • отдельный обработчик ошибок
  • независимый lifecycle
  • собственный mutation key (если задан)

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

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 и mutateAsync становится критичным при параллельном выполнении.

  • mutate — fire-and-forget подход
  • mutateAsync — возвращает Promise и позволяет управлять порядком выполнения
await Promise.all([
  mutationA.mutateAsync({ name: "Alex" }),
  mutationB.mutateAsync({ theme: "dark" }),
]);

Использование Promise.all позволяет синхронизировать завершение группы мутаций, сохраняя их параллельное выполнение.


Координация параллельных мутаций через 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 при параллельных вызовах

Mutation cache хранит все активные мутации независимо друг от друга. При параллельном запуске формируется несколько записей с разными идентификаторами.

Каждая запись содержит:

  • variables
  • status
  • error
  • data
  • meta-информацию

Параллельное выполнение приводит к одновременному существованию нескольких pending состояний.

const mutation = useMutation({
  mutationFn: api.saveItem,
  mutationKey: ["save-item"],
});

Если вызвать mutate несколько раз подряд, TanStack Query создаст отдельные инстансы операций внутри cache.


Конкурентность и гонки состояний

Параллельные мутации могут приводить к race conditions при работе с shared state.

Типичные сценарии конфликтов:

  • два запроса обновляют одну сущность
  • optimistic update перезаписывает результат другого запроса
  • инвалидация кеша выполняется несколько раз подряд
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

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

  • несколько isPending могут быть активны одновременно
  • общий loading-state требует агрегации
  • ошибки разных мутаций должны быть разделены
const isLoading =
  mutationA.isPending || mutationB.isPending || mutationC.isPending;

Агрегация состояния становится частью архитектуры при массовых обновлениях.


Интеграция с query cache при массовых обновлениях

После параллельных мутаций чаще всего требуется синхронизация query cache.

await Promise.all([
  mutationA.mutateAsync(dataA),
  mutationB.mutateAsync(dataB),
]);

queryClient.setQueryData(["items"], (old) =>
  old.map(applyLocalChanges)
);

Локальное обновление кеша может уменьшить количество refetch-запросов и стабилизировать UI после серии параллельных операций.


Поведение retry при параллельных мутациях

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

useMutation({
  mutationFn: api.save,
  retry: 3,
});

Если одновременно запущено несколько мутаций, каждая может находиться в собственном retry-loop, что увеличивает нагрузку на сервер при массовых сбоях.


Синхронизация через mutationKey

Хотя mutationKey не влияет на кеширование как в queries, он позволяет группировать мутации для аналитики и контроля.

useMutation({
  mutationKey: ["user", "bulk-update"],
  mutationFn: api.bulkUpdate,
});

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


Архитектурные паттерны параллельных мутаций

На практике используются несколько устойчивых моделей:

  • fan-out / fan-in: разбиение операции на несколько параллельных запросов с последующей синхронизацией результата
  • bulk mutations: единый endpoint вместо множества параллельных вызовов
  • staged execution: частичная параллельность с контрольными точками синхронизации

Выбор модели зависит от стоимости запросов, консистентности данных и нагрузки на backend.