Мутации в TanStack Query предназначены для изменения данных на сервере: создания, обновления, удаления записей, отправки форм, изменения состояния объектов и выполнения любых операций записи.
После успешного завершения мутации приложение обычно должно:
Для этого в TanStack Query используется обработчик
onSuccess.
Базовый пример:
const mutation = useMutation({
mutationFn: createPost,
onSuccess: (data) => {
console.log('Мутация выполнена успешно');
console.log(data);
}
});
Параметр data содержит результат, возвращённый
сервером.
Например:
async function createPost(post) {
const response = await fetch('/api/posts', {
method: 'POST',
body: JSON.stringify(post)
});
return response.json();
}
Если сервер возвращает:
{
"id": 15,
"title": "Новая статья"
}
то data внутри onSuccess будет содержать
этот объект.
Обработчик получает несколько аргументов:
onSuccess: (data, variables, context) => {
}
Результат успешной мутации.
onSuccess: (data) => {
console.log(data.id);
}
Аргументы, переданные в mutate.
mutation.mutate({
title: 'React'
});
onSuccess: (data, variables) => {
console.log(variables.title);
}
Контекст, возвращённый из onMutate.
Используется при optimistic update.
onMutate: async (newTodo) => {
return {
previousTodos: queryClient.getQueryData(['todos'])
};
}
onSuccess: (data, variables, context) => {
console.log(context.previousTodos);
}
Одна из главных задач onSuccess — синхронизация
кэша.
Самый распространённый подход — пометить запросы устаревшими.
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: createPost,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ['posts']
});
}
});
После инвалидации TanStack Query автоматически выполнит повторный запрос.
Инвалидация:
Особенно полезна, когда:
Иногда повторный запрос не нужен.
Если сервер уже вернул актуальные данные, можно обновить кэш вручную.
onSuccess: (newPost) => {
queryClient.setQueryData(
['posts'],
(oldPosts = []) => {
return [...oldPosts, newPost];
}
);
}
Повторный HTTP-запрос отсутствует.
Не происходит дополнительного обращения к серверу.
Интерфейс обновляется сразу после ответа сервера.
Ручное обновление сложнее поддерживать.
Проблемы возникают, если:
Поэтому invalidateQueries используется чаще.
Пример изменения одной записи:
onSuccess: (updatedPost) => {
queryClient.setQueryData(
['posts'],
(oldPosts = []) => {
return oldPosts.map(post =>
post.id === updatedPost.id
? updatedPost
: post
);
}
);
}
Часто существует отдельный запрос детали сущности.
Например:
useQuery({
queryKey: ['post', postId],
queryFn: fetchPost
});
После успешного обновления записи можно синхронизировать этот кэш:
onSuccess: (updatedPost) => {
queryClient.setQueryData(
['post', updatedPost.id],
updatedPost
);
}
В реальных приложениях данные часто дублируются.
Например:
После успешной мутации иногда необходимо обновить несколько запросов.
onSuccess: (updatedPost) => {
queryClient.setQueryData(
['post', updatedPost.id],
updatedPost
);
queryClient.invalidateQueries({
queryKey: ['posts']
});
queryClient.invalidateQueries({
queryKey: ['favorites']
});
}
onSuccess поддерживает асинхронный код.
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: ['posts']
});
console.log('Кэш обновлён');
}
TanStack Query дождётся завершения Promise.
Иногда операции должны выполняться строго по порядку.
onSuccess: async (data) => {
await queryClient.invalidateQueries({
queryKey: ['posts']
});
navigate(`/posts/${data.id}`);
}
В этом примере переход произойдёт только после обновления данных.
Очень распространённый сценарий.
onSuccess: () => {
toast.success('Статья сохранена');
}
После успешной отправки форма часто сбрасывается.
onSuccess: () => {
reset();
}
onSuccess: () => {
setIsModalOpen(false);
}
onSuccess: (data) => {
navigate(`/posts/${data.id}`);
}
Обычно onSuccess содержит сразу несколько операций.
onSuccess: async (data) => {
queryClient.invalidateQueries({
queryKey: ['posts']
});
toast.success('Пост создан');
reset();
navigate(`/posts/${data.id}`);
}
Обработчики можно передавать непосредственно в
mutate.
mutation.mutate(
values,
{
onSuccess: () => {
console.log('Успех');
}
}
);
Определяется внутри useMutation.
useMutation({
mutationFn,
onSuccess: () => {}
});
Подходит для:
Передаётся в mutate.
mutate(data, {
onSuccess: () => {}
});
Подходит для:
Сначала вызывается глобальный onSuccess, затем
локальный.
const mutation = useMutation({
mutationFn,
onSuccess: () => {
console.log('global');
}
});
mutation.mutate(data, {
onSuccess: () => {
console.log('local');
}
});
Результат:
global
local
mutateAsync позволяет работать через
async/await.
const mutation = useMutation({
mutationFn: createPost
});
const handleSubmit = async () => {
try {
const data = await mutation.mutateAsync(values);
console.log(data);
} catch (error) {
console.log(error);
}
};
mutateAsync полезен:
Оба механизма могут использоваться одновременно.
const mutation = useMutation({
mutationFn: createPost,
onSuccess: () => {
console.log('Глобальная логика');
}
});
await mutation.mutateAsync(values);
Во многих проектах применяется следующая схема:
Плохой пример:
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ['posts']
});
queryClient.invalidateQueries({
queryKey: ['users']
});
queryClient.invalidateQueries({
queryKey: ['comments']
});
queryClient.invalidateQueries({
queryKey: ['stats']
});
}
Большое количество ручных инвалидаций усложняет поддержку.
Полезно выносить обновление кэша в отдельные функции.
function invalidatePostQueries(queryClient) {
queryClient.invalidateQueries({
queryKey: ['posts']
});
queryClient.invalidateQueries({
queryKey: ['post-stats']
});
}
onSuccess: () => {
invalidatePostQueries(queryClient);
}
Распространённый подход:
function useCreatePost() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: createPost,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ['posts']
});
}
});
}
Компонент получает готовую бизнес-логику.
После удаления запись обычно убирается из кэша.
onSuccess: (_, deletedId) => {
queryClient.setQueryData(
['posts'],
(oldPosts = []) => {
return oldPosts.filter(
post => post.id !== deletedId
);
}
);
}
onSuccess: (newPost) => {
queryClient.setQueryData(
['posts'],
(oldPosts = []) => {
return [newPost, ...oldPosts];
}
);
}
onSuccess: (updatedPost) => {
queryClient.setQueryData(
['posts'],
(oldPosts = []) => {
return oldPosts.map(post =>
post.id === updatedPost.id
? updatedPost
: post
);
}
);
}
Если запрос имеет большой staleTime, то без
invalidateQueries интерфейс может продолжать показывать
устаревшие данные.
useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
staleTime: 1000 * 60 * 10
});
В таком случае успешная мутация особенно нуждается в синхронизации кэша.
При создании сущности сервер часто генерирует id.
onSuccess: (createdUser) => {
console.log(createdUser.id);
}
Это важно для:
Частые глобальные инвалидации могут приводить к лишним запросам.
Ручное обновление нескольких запросов легко приводит к ошибкам.
Плохой пример:
onSuccess: () => {
toast.success('Успех');
openModal();
setSidebarState();
updateTheme();
playAnimation();
}
Мутация становится слишком зависимой от конкретного интерфейса.
Внутри mutation hooks:
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ['posts']
});
}
На уровне компонентов:
mutate(data, {
onSuccess: () => {
toast.success('Сохранено');
}
});
При optimistic update успешная мутация подтверждает временные изменения.
onMutate: async (newPost) => {
const previousPosts =
queryClient.getQueryData(['posts']);
queryClient.setQueryData(
['posts'],
(old = []) => [...old, newPost]
);
return { previousPosts };
}
После успеха:
onSuccess: (savedPost) => {
queryClient.invalidateQueries({
queryKey: ['posts']
});
}
Грамотная обработка успешных мутаций обеспечивает: