Последовательные мутации

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

Типичные области применения:

  • цепочки создания связанных сущностей (создать пользователя → создать профиль → привязать настройки)
  • пошаговые бизнес-процессы (оформление заказа с несколькими этапами)
  • миграции данных
  • последовательные обновления связанных ресурсов

Ключевая характеристика — контроль над порядком выполнения через async/await и mutateAsync, а не параллельные вызовы.


Базовая модель useMutation

Мутации в TanStack Query строятся вокруг useMutation, которая инкапсулирует побочные эффекты изменения данных на сервере.

import { useMutation } from '@tanstack/react-query'

const createUserMutation = useMutation({
  mutationFn: async (newUser) => {
    const res = await fetch('/api/users', {
      method: 'POST',
      body: JSON.stringify(newUser),
    })
    return res.json()
  },
})

В базовой форме mutate запускает мутацию без возможности ожидания результата, тогда как mutateAsync возвращает Promise, что делает его основным инструментом для последовательного выполнения.


Последовательное выполнение через async/await

Последовательность мутаций в TanStack Query реализуется через явное ожидание завершения каждой операции.

const runSequence = async () => {
  const user = await createUserMutation.mutateAsync({
    name: 'Alex',
  })

  const profile = await createProfileMutation.mutateAsync({
    userId: user.id,
    theme: 'dark',
  })

  await createSettingsMutation.mutateAsync({
    profileId: profile.id,
    notifications: true,
  })
}

В этой модели каждая мутация получает данные от предыдущей, что гарантирует корректную связку зависимостей.


Цепочки mutateAsync

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

createUserMutation
  .mutateAsync({ name: 'Alex' })
  .then((user) => {
    return createProfileMutation.mutateAsync({
      userId: user.id,
    })
  })
  .then((profile) => {
    return createSettingsMutation.mutateAsync({
      profileId: profile.id,
    })
  })

Хотя Promise-цепочки сохраняют порядок, в современных приложениях предпочтение отдается async/await из-за лучшей читаемости и упрощенной обработки ошибок.


Последовательные мутации в цикле

Частый сценарий — последовательная обработка массива, где каждая мутация зависит от завершения предыдущей.

const createManyPosts = async (posts) => {
  const results = []

  for (const post of posts) {
    const created = await createPostMutation.mutateAsync(post)
    results.push(created)
  }

  return results
}

Использование for...of критично, поскольку методы вроде forEach не поддерживают корректное ожидание await.


Динамическая последовательность через reduce

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

const runPipeline = (items) => {
  return items.reduce((promise, item) => {
    return promise.then((acc) => {
      return createStepMutation.mutateAsync(item).then((result) => {
        acc.push(result)
        return acc
      })
    })
  }, Promise.resolve([]))
}

Этот паттерн полезен в случаях, когда требуется аккумуляция результатов каждого шага.


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

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

Остановка цепочки при ошибке

const executeFlow = async () => {
  const user = await createUserMutation.mutateAsync(data)

  const profile = await createProfileMutation.mutateAsync({
    userId: user.id,
  })

  await createSettingsMutation.mutateAsync({
    profileId: profile.id,
  })
}

Любая ошибка автоматически прерывает выполнение.

Продолжение выполнения

const safeExecute = async (items) => {
  const results = []

  for (const item of items) {
    try {
      const res = await createItemMutation.mutateAsync(item)
      results.push(res)
    } catch (e) {
      results.push({ error: true, item })
    }
  }

  return results
}

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


Инвалидация кеша после последовательных мутаций

После завершения цепочки мутаций важно синхронизировать состояние кеша через queryClient.invalidateQueries.

const queryClient = useQueryClient()

const runSequence = async () => {
  await createUserMutation.mutateAsync(data)
  await createProfileMutation.mutateAsync(profileData)
  await createSettingsMutation.mutateAsync(settingsData)

  await queryClient.invalidateQueries({
    queryKey: ['user'],
  })

  await queryClient.invalidateQueries({
    queryKey: ['profile'],
  })
}

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


Зависимые последовательные мутации

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

const processOrder = async (cart) => {
  const order = await createOrderMutation.mutateAsync({
    items: cart.items,
  })

  const payment = await payOrderMutation.mutateAsync({
    orderId: order.id,
    amount: order.total,
  })

  const receipt = await createReceiptMutation.mutateAsync({
    paymentId: payment.id,
  })

  return receipt
}

Такая структура формирует линейный бизнес-процесс с жесткой зависимостью шагов.


Контроль конкурентности и предотвращение гонок

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

Антипаттерн:

createUserMutation.mutateAsync(data)
createUserMutation.mutateAsync(data2)

В этом случае порядок выполнения не гарантируется.

Корректный вариант:

await createUserMutation.mutateAsync(data)
await createUserMutation.mutateAsync(data2)

Минимизация побочных перерендеров

При последовательных мутациях важно учитывать количество обновлений состояния React Query. Каждая мутация может триггерить обновление кеша и перерендер компонентов.

Практика оптимизации:

  • группировка invalidateQueries после цепочки
  • использование setQueryData вместо повторных fetch при известных данных
  • разделение UI-обновлений и серверных операций
await queryClient.setQueryData(['user'], user)
await queryClient.setQueryData(['profile'], profile)

Пошаговые workflow как единая транзакция

Последовательные мутации часто моделируют поведение транзакции на клиентском уровне.

const executeTransaction = async () => {
  const created = []

  try {
    const user = await createUserMutation.mutateAsync(data)
    created.push(['user', user])

    const profile = await createProfileMutation.mutateAsync({
      userId: user.id,
    })
    created.push(['profile', profile])

    const settings = await createSettingsMutation.mutateAsync({
      profileId: profile.id,
    })
    created.push(['settings', settings])
  } catch (e) {
    // компенсационные действия
    for (const [type, entity] of created.reverse()) {
      await rollbackMutation.mutateAsync({ type, id: entity.id })
    }

    throw e
  }
}

Такой подход имитирует компенсируемые транзакции в распределенных системах.


Антипаттерны последовательных мутаций

Параллельный запуск зависимых операций

const user = createUserMutation.mutateAsync(data)
const profile = createProfileMutation.mutateAsync({ userId: user.id })

Ошибка: user.id недоступен до завершения первой операции.

Игнорирование await

createUserMutation.mutateAsync(data)
createProfileMutation.mutateAsync(profileData)

Результат становится недетерминированным.

Перегрузка invalidateQueries внутри цепочки

Множественные инвалидации между шагами приводят к избыточным refetch-запросам и деградации производительности.


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

В крупных приложениях последовательные мутации выделяются в отдельный слой — orchestration layer, который не привязан к UI.

export const userOnboardingFlow = async ({
  userData,
  profileData,
  settingsData,
}) => {
  const user = await api.createUser(userData)

  const profile = await api.createProfile({
    ...profileData,
    userId: user.id,
  })

  const settings = await api.createSettings({
    ...settingsData,
    profileId: profile.id,
  })

  return { user, profile, settings }
}

React Query в этом случае используется как транспортный слой, а не как координатор бизнес-логики.


Управление последовательностью через mutation state

TanStack Query предоставляет mutationKey, позволяющий логически группировать мутации, что упрощает контроль состояния последовательностей.

const mutation = useMutation({
  mutationKey: ['user-flow'],
  mutationFn: createUser,
})

При сложных сценариях последовательных операций это помогает отслеживать контекст выполнения в mutationCache.


Структурирование длинных цепочек операций

При увеличении числа шагов линейные цепочки становятся трудно поддерживаемыми. Альтернативный подход — декларативный pipeline.

const steps = [
  (ctx) => createUserMutation.mutateAsync(ctx),
  (ctx) => createProfileMutation.mutateAsync(ctx),
  (ctx) => createSettingsMutation.mutateAsync(ctx),
]

const runSteps = async (initial) => {
  let ctx = initial

  for (const step of steps) {
    ctx = await step(ctx)
  }

  return ctx
}

Такой подход повышает читаемость и масштабируемость последовательных процессов.