Последовательные мутации представляют собой выполнение нескольких
операций изменения данных на сервере строго по порядку, где каждая
следующая операция зависит от результата предыдущей. В контексте RTK
Query это чаще всего означает цепочку mutation-запросов,
выполняемых через trigger() с ожиданием завершения каждого
шага.
Ключевая особенность подхода заключается в контроле асинхронного потока: каждая мутация должна быть завершена (успешно или с обработанной ошибкой) перед запуском следующей.
RTK Query предоставляет useMutation хук, возвращающий
функцию-триггер и объект состояния.
const [createUser] = useCreateUserMutation();
const [updateProfile] = useUpdateProfileMutation();
const [uploadAvatar] = useUploadAvatarMutation();
Последовательное выполнение строится вокруг
async/await:
const handleSubmit = async () => {
const user = await createUser({ name: "Alex" }).unwrap();
const profile = await updateProfile({ userId: user.id, bio: "Dev" }).unwrap();
const avatar = await uploadAvatar({ userId: user.id, file: blob }).unwrap();
return { user, profile, avatar };
};
Метод unwrap() преобразует результат мутации в обычный
Promise:
payloadЭто позволяет использовать стандартные конструкции управления потоком:
try {
const user = await createUser(data).unwrap();
const profile = await updateProfile({ userId: user.id }).unwrap();
} catch (err) {
console.error("Ошибка последовательной мутации", err);
}
Часто каждая следующая операция требует данных предыдущей. Это формирует цепочку зависимостей:
Пример:
const result = await createOrder(orderData).unwrap();
await addOrderItems({
orderId: result.id,
items: cartItems
}).unwrap();
await confirmOrder({
orderId: result.id
}).unwrap();
Более низкоуровневый контроль возможен через
onQueryStarted, где допускается выполнение побочных
эффектов до завершения основного запроса.
createPost: builder.mutation({
query: (data) => ({
url: "/posts",
method: "POST",
body: data
}),
async onQueryStarted(arg, { dispatch, queryFulfilled }) {
const { data } = await queryFulfilled;
await dispatch(
api.endpoints.addTags.initiate({
postId: data.id,
tags: arg.tags
})
).unwrap();
}
});
Здесь важно, что последовательность мутаций выполняется внутри жизненного цикла одного endpoint, но каждая операция остаётся независимой.
RTK Query позволяет вызывать мутации не только через hooks, но и
через dispatch.
const result = await dispatch(
api.endpoints.createUser.initiate(userData)
).unwrap();
await dispatch(
api.endpoints.assignRole.initiate({
userId: result.id,
role: "admin"
})
).unwrap();
Такой подход полезен:
Главная сложность последовательных мутаций — остановка цепочки при ошибке.
try {
const user = await createUser(data).unwrap();
const profile = await updateProfile({
userId: user.id
}).unwrap();
await sendWelcomeEmail({ userId: user.id }).unwrap();
} catch (e) {
// вся цепочка прерывается на первом сбое
console.error("Pipeline failed", e);
}
Важно, что RTK Query не откатывает предыдущие мутации автоматически. Каждая операция уже могла изменить серверное состояние.
Для имитации транзакционности используется ручной откат:
try {
const user = await createUser(data).unwrap();
try {
await createBillingAccount({ userId: user.id }).unwrap();
} catch (e) {
await deleteUser({ id: user.id }).unwrap();
throw e;
}
} catch (e) {
console.error("Ошибка с компенсацией", e);
}
Такой подход часто называют “saga-like orchestration”.
Каждая мутация может влиять на кэш через:
invalidatesTagsupdateQueryDataonQueryStarted optimistic updatesПри последовательных мутациях важно учитывать порядок инвалидции:
createPost: builder.mutation({
query: (data) => ({ url: "/posts", method: "POST", body: data }),
invalidatesTags: ["Posts"]
});
addComment: builder.mutation({
query: (data) => ({ url: "/comments", method: "POST", body: data }),
invalidatesTags: (result, error, arg) => [
{ type: "Post", id: arg.postId }
]
});
Если такие мутации выполняются последовательно, возможны:
Лучший вариант — заменить цепочку одним endpoint:
createUserWithProfile: builder.mutation({
query: (data) => ({
url: "/user/full",
method: "POST",
body: data
})
});
const payload = {
user: userData,
profile: profileData,
settings: settingsData
};
await createFullUser(payload).unwrap();
При цепочках мутаций часто выгодно:
Последовательные:
await a().unwrap();
await b().unwrap();
await c().unwrap();
Параллельные:
await Promise.all([
a().unwrap(),
b().unwrap(),
c().unwrap()
]);
Последовательный подход выбирается, если:
RTK Query предоставляет состояние только для одной мутации, поэтому для цепочек используется локальная агрегация:
const [loading, setLoading] = useState(false);
const run = async () => {
setLoading(true);
try {
const a = await stepA().unwrap();
const b = await stepB({ id: a.id }).unwrap();
const c = await stepC({ id: b.id }).unwrap();
} finally {
setLoading(false);
}
};
При последовательных мутациях важно учитывать:
Часто используется блокировка:
if (isRunning) return;
или отмена предыдущего процесса через AbortController, который RTK Query поддерживает на уровне запросов.
При увеличении сложности цепочек мутации превращаются в мини-саги:
const runPipeline = async (input) => {
const user = await createUser(input).unwrap();
if (input.withBilling) {
await createBilling(user.id).unwrap();
}
if (input.withEmail) {
await setupEmail(user.id).unwrap();
}
return user;
};
Последовательные мутации в RTK Query формируют прикладной слой оркестрации, в котором: