Основы использования useMutation

Механизм useMutation в RTK Query предназначен для выполнения операций, изменяющих состояние на сервере: создание, обновление, удаление сущностей, выполнение командных запросов. В отличие от useQuery, который ориентирован на получение и кэширование данных, useMutation работает по событийному принципу: запрос инициируется явно и не выполняется автоматически при рендере.


Общая модель работы useMutation

useMutation возвращает кортеж из двух элементов:

  • функция запуска запроса (trigger)
  • объект состояния текущей мутации

Основная особенность заключается в том, что запрос выполняется только после вызова trigger.

const [createUser, result] = useCreateUserMutation();

Здесь:

  • createUser — функция запуска запроса
  • result — объект с состоянием выполнения мутации

Объект результата мутации

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

  • data — результат успешного выполнения
  • error — информация об ошибке
  • isLoading — запрос выполняется впервые
  • isSuccess — запрос завершён успешно
  • isError — произошла ошибка
  • isUninitialized — мутация ещё не запускалась
  • status — строковый статус (pending, fulfilled, rejected, uninitialized)
const [createPost, result] = useCreatePostMutation();

const {
  data,
  error,
  isLoading,
  isSuccess,
  isError,
  isUninitialized
} = result;

Эти поля позволяют полностью контролировать UI-состояние без дополнительного локального state.


Базовое определение endpoint для мутации

Мутации описываются в createApi через поле mutation.

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

export const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  endpoints: (builder) => ({
    createPost: builder.mutation({
      query: (newPost) => ({
        url: '/posts',
        method: 'POST',
        body: newPost,
      }),
    }),
  }),
});

export const { useCreatePostMutation } = api;

Запуск мутации

Функция trigger принимает аргумент, который передаётся в query.

const [createPost] = useCreatePostMutation();

createPost({
  title: 'Post title',
  content: 'Post content',
});

При вызове происходит:

  1. формирование запроса
  2. отправка на сервер
  3. обновление состояния мутации
  4. возврат промиса

Promise-объект и unwrap

Функция trigger возвращает промис, содержащий результат выполнения запроса. RTK Query предоставляет метод unwrap, позволяющий получить “чистые” данные или выбросить ошибку.

const [createPost] = useCreatePostMutation();

try {
  const result = await createPost({ title: 'New post' }).unwrap();
  console.log(result);
} catch (err) {
  console.error(err);
}

Особенность unwrap

  • при успехе возвращаются данные из response
  • при ошибке выбрасывается исключение
  • упрощается работа с async/await без проверки error

Состояния загрузки и повторные вызовы

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

const [updateUser, result] = useUpdateUserMutation();

const onCl ick = () => {
  updateUser({ id: 1, name: 'New name' });
  updateUser({ id: 1, name: 'Another name' });
};

Каждый вызов обновляет состояние result, при этом предыдущий запрос не блокирует следующий.


Передача параметров в мутацию

Параметры передаются напрямую в trigger и становятся входом для query.

builder.mutation({
  query: ({ id, ...patch }) => ({
    url: `/users/${id}`,
    method: 'PATCH',
    body: patch,
  }),
});

Вызов:

updateUser({
  id: 10,
  name: 'Updated name',
});

Инвалидация кэша после мутации

Одной из ключевых возможностей RTK Query является автоматическое обновление кэша через tags.

endpoints: (builder) => ({
  addPost: builder.mutation({
    query: (post) => ({
      url: '/posts',
      method: 'POST',
      body: post,
    }),
    invalidatesTags: ['Posts'],
  }),

  getPosts: builder.query({
    query: () => '/posts',
    providesTags: ['Posts'],
  }),
});

После успешного выполнения addPost все запросы, подписанные на Posts, автоматически обновляются.


Оптимистические обновления

RTK Query поддерживает возможность обновления UI до завершения запроса.

addPost: builder.mutation({
  query: (post) => ({
    url: '/posts',
    method: 'POST',
    body: post,
  }),
  async onQueryStarted(post, { dispatch, queryFulfilled }) {
    const patchResult = dispatch(
      api.util.updateQueryData('getPosts', undefined, (draft) => {
        draft.push(post);
      })
    );

    try {
      await queryFulfilled;
    } catch {
      patchResult.undo();
    }
  },
});

Поведение:

  • данные добавляются в кэш мгновенно
  • при ошибке происходит откат изменений

Различие между useMutation и useQuery

useQuery

  • выполняется автоматически
  • ориентирован на чтение данных
  • кэшируется и подписывается на обновления

useMutation

  • запускается вручную
  • ориентирован на изменение данных
  • не подписан на кэш напрямую

Повторные попытки и обработка ошибок

Мутации не выполняют автоматический retry по умолчанию, но поведение можно настроить на уровне baseQuery.

baseQuery: fetchBaseQuery({
  baseUrl: '/api',
  timeout: 5000,
});

Ошибки возвращаются в объекте результата:

if (result.isError) {
  console.log(result.error);
}

Сброс состояния мутации

Состояние мутации можно сбрасывать вручную через reset.

const [createPost, { reset }] = useCreatePostMutation();

reset();

Это приводит объект состояния к начальному виду:

  • isUninitialized = true
  • data = undefined
  • error = undefined

Использование в асинхронных сценариях

Мутации удобно комбинируются с асинхронной логикой:

const [login] = useLoginMutation();

const handleLogin = async () => {
  const user = await login(credentials).unwrap();

  localStorage.setItem('token', user.token);
};

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

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

  • каждая получает собственное состояние
  • результаты не смешиваются
  • последний вызов не отменяет предыдущие

Сигнатура hook и типизация (TypeScript-совместимость)

Хотя рассматривается JavaScript, RTK Query изначально типизирован, и сигнатура выглядит следующим образом:

const [trigger, result] = useMutation();

Тип результата зависит от описания endpoint:

  • TData — тип ответа
  • TArg — тип аргумента
  • TError — тип ошибки

Контроль жизненного цикла запроса

Жизненный цикл мутации проходит стадии:

  1. uninitialized
  2. pending
  3. fulfilled или rejected

Каждая стадия отражается в состоянии:

result.status

Использование с побочными эффектами

Мутации часто используются совместно с onSuccess логикой через useEffect:

const [createPost, result] = useCreatePostMutation();

useEffect(() => {
  if (result.isSuccess) {
    console.log('Создание завершено');
  }
}, [result.isSuccess]);

Доступ к исходному промису и управлению потоками

Trigger возвращает промис, который можно использовать как обычный async-оператор:

createPost(data)
  .unwrap()
  .then((res) => {
    console.log(res);
  })
  .catch((err) => {
    console.error(err);
  });

Ключевые особенности поведения useMutation

  • отсутствие автоматического запуска
  • управление через trigger-функцию
  • интеграция с Redux cache через tags
  • поддержка optimistic updates
  • возможность работы через async/await
  • независимость состояний при множественных вызовах