Последовательные мутации в TanStack Query представляют собой строго упорядоченное выполнение операций изменения состояния сервера, при котором каждая следующая мутация начинается только после завершения предыдущей. Такой подход критичен в сценариях, где результат одной операции является входными данными для следующей, либо где важен порядок применения изменений.
Типичные области применения:
Ключевая характеристика — контроль над порядком выполнения через
async/await и mutateAsync, а не параллельные
вызовы.
Мутации в 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, что делает его основным инструментом для последовательного
выполнения.
Последовательность мутаций в 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,
})
}
В этой модели каждая мутация получает данные от предыдущей, что гарантирует корректную связку зависимостей.
Последовательные мутации могут быть организованы как линейная цепочка без промежуточных переменных, но с сохранением контроля над результатами.
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.
Функциональный подход позволяет строить цепочку мутаций без явного цикла.
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 при
известных данныхawait queryClient.setQueryData(['user'], user)
await queryClient.setQueryData(['profile'], profile)
Последовательные мутации часто моделируют поведение транзакции на клиентском уровне.
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 недоступен до завершения первой
операции.
createUserMutation.mutateAsync(data)
createProfileMutation.mutateAsync(profileData)
Результат становится недетерминированным.
Множественные инвалидации между шагами приводят к избыточным 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 в этом случае используется как транспортный слой, а не как координатор бизнес-логики.
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
}
Такой подход повышает читаемость и масштабируемость последовательных процессов.