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

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

Последовательные мутации представляют собой выполнение нескольких операций изменения данных на сервере строго по порядку, где каждая следующая операция зависит от результата предыдущей. В контексте 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() как ключевой механизм управления потоком

Метод unwrap() преобразует результат мутации в обычный Promise:

  • при успехе возвращает payload
  • при ошибке выбрасывает исключение

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

try {
  const user = await createUser(data).unwrap();
  const profile = await updateProfile({ userId: user.id }).unwrap();
} catch (err) {
  console.error("Ошибка последовательной мутации", err);
}

Зависимые мутации и передача контекста

Часто каждая следующая операция требует данных предыдущей. Это формирует цепочку зависимостей:

  1. Создание сущности
  2. Обогащение данных
  3. Связывание с другими сущностями
  4. Финальная настройка

Пример:

const result = await createOrder(orderData).unwrap();

await addOrderItems({
  orderId: result.id,
  items: cartItems
}).unwrap();

await confirmOrder({
  orderId: result.id
}).unwrap();

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

Более низкоуровневый контроль возможен через 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, но каждая операция остаётся независимой.


Цепочки мутаций через dispatch initiate

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();

Такой подход полезен:

  • в Redux-thunk
  • в сложных бизнес-операциях
  • при оркестрации нескольких endpoint’ов

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

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

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 не откатывает предыдущие мутации автоматически. Каждая операция уже могла изменить серверное состояние.


Компенсирующие операции (ручной rollback)

Для имитации транзакционности используется ручной откат:

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”.


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

Каждая мутация может влиять на кэш через:

  • invalidatesTags
  • updateQueryData
  • onQueryStarted 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 }
  ]
});

Если такие мутации выполняются последовательно, возможны:

  • лишние перезапросы
  • гонки обновления кэша
  • промежуточные неконсистентные состояния UI

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

1. Объединение серверных операций

Лучший вариант — заменить цепочку одним endpoint:

createUserWithProfile: builder.mutation({
  query: (data) => ({
    url: "/user/full",
    method: "POST",
    body: data
  })
});

2. Локальная агрегация перед отправкой

const payload = {
  user: userData,
  profile: profileData,
  settings: settingsData
};

await createFullUser(payload).unwrap();

3. Минимизация промежуточных invalidation

При цепочках мутаций часто выгодно:

  • отключить частичную инвалидацию
  • инвалидировать только после финального шага

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

Последовательные:

await a().unwrap();
await b().unwrap();
await c().unwrap();

Параллельные:

await Promise.all([
  a().unwrap(),
  b().unwrap(),
  c().unwrap()
]);

Последовательный подход выбирается, если:

  • есть зависимости между результатами
  • важен порядок
  • операции изменяют одно состояние

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

Пайплайн создания сущности

  1. createEntity
  2. attachRelations
  3. uploadAssets
  4. finalize

Авторизация и настройка профиля

  1. login
  2. fetch profile
  3. update preferences
  4. preload cache

Импорт данных

  1. upload file
  2. parse server-side
  3. store chunks
  4. commit import

Управление состоянием загрузки в цепочках

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 формируют прикладной слой оркестрации, в котором:

  • каждый endpoint остаётся независимым
  • порядок контролируется на уровне бизнес-логики
  • кэш управляется декларативно через tags и manual updates
  • транзакционность не гарантируется и моделируется вручную при необходимости