Мутации в TanStack Query используются для изменения данных на
сервере: создания, обновления, удаления и отправки форм. В отличие от
useQuery, где ошибки часто связаны с чтением данных, ошибки
мутаций обычно возникают в критических пользовательских сценариях:
Из-за этого обработка ошибок в мутациях требует более точного контроля. Необходимо учитывать:
isErrorНаиболее простой способ обработки — использование состояния
isError.
import { useMutation } from '@tanstack/react-query'
import axios from 'axios'
function CreatePost() {
const mutation = useMutation({
mutationFn: async (post) => {
const response = await axios.post('/api/posts', post)
return response.data
}
})
const handleCreate = () => {
mutation.mutate({
title: 'Новая статья'
})
}
return (
<div>
<button onCl ick={handleCreate}>
Создать
</button>
{mutation.isPending && <p>Сохранение...</p>}
{mutation.isError && (
<p>Ошибка при создании записи</p>
)}
{mutation.isSuccess && (
<p>Запись создана</p>
)}
</div>
)
}
Состояние isError становится true, если
mutationFn выбрасывает исключение.
errorДля получения текста ошибки используется поле error.
if (mutation.isError) {
console.log(mutation.error)
}
Пример:
{mutation.isError && (
<p>{mutation.error.message}</p>
)}
Axios автоматически выбрасывает исключение при статусах
4xx и 5xx.
const mutation = useMutation({
mutationFn: async (data) => {
const response = await axios.post('/api/users', data)
return response.data
}
})
Если сервер вернёт:
{
"message": "Email уже существует"
}
то ошибка будет доступна через:
mutation.error.response.data.message
Пример:
{mutation.isError && (
<p>
{mutation.error.response.data.message}
</p>
)}
onErrorTanStack Query позволяет централизованно обрабатывать ошибки через
callback onError.
const mutation = useMutation({
mutationFn: createUser,
onError: (error) => {
console.error(error)
}
})
onError вызывается:
isError;onErrorCallback получает несколько аргументов.
const mutation = useMutation({
mutationFn: updatePost,
onError: (error, variables, context) => {
console.log(error)
console.log(variables)
console.log(context)
}
})
errorОбъект ошибки.
variablesАргументы, переданные в mutate.
mutation.mutate({
id: 10,
title: 'Новый текст'
})
Тогда:
variables.id
variables.title
contextКонтекст, возвращённый из onMutate.
Обычно используется для rollback optimistic update.
Важно различать типы ошибок.
Сервер ответил кодом:
400401403404422500Пример:
error.response
Запрос вообще не дошёл до сервера:
Пример:
error.request
onError: (error) => {
if (error.response) {
console.log('Ошибка сервера')
} else if (error.request) {
console.log('Сетевая ошибка')
} else {
console.log('Неизвестная ошибка')
}
}
mutatemutate поддерживает локальные обработчики.
mutation.mutate(data, {
onError: (error) => {
console.log(error)
}
})
Это позволяет переопределять логику для конкретного вызова.
onError и локальный onErrorОба обработчика могут существовать одновременно.
const mutation = useMutation({
mutationFn: saveUser,
onError: () => {
console.log('Глобальная ошибка')
}
})
mutation.mutate(data, {
onError: () => {
console.log('Локальная ошибка')
}
})
Будут вызваны оба обработчика.
mutateAsyncmutateAsync превращает мутацию в Promise.
const mutation = useMutation({
mutationFn: login
})
Пример:
try {
const result = await mutation.mutateAsync({
email,
password
})
console.log(result)
} catch (error) {
console.log(error)
}
Такой подход особенно полезен:
try/catch не работает с mutatemutate не возвращает Promise.
Неправильно:
try {
mutation.mutate(data)
} catch (error) {
console.log(error)
}
Правильно:
await mutation.mutateAsync(data)
Очень распространённый сценарий.
const mutation = useMutation({
mutationFn: registerUser
})
Пример:
const handleSubmit = async (values) => {
try {
await mutation.mutateAsync(values)
resetForm()
} catch (error) {
setFormError(
error.response.data.message
)
}
}
Backend часто возвращает ошибки валидации.
Пример ответа сервера:
{
"errors": {
"email": ["Некорректный email"],
"password": ["Минимум 8 символов"]
}
}
Обработка:
catch (error) {
const errors =
error.response.data.errors
setErrors(errors)
}
По умолчанию мутации не повторяются автоматически.
Это важное отличие от useQuery.
Причина:
const mutation = useMutation({
mutationFn: saveData,
retry: 3
})
Теперь TanStack Query выполнит:
Можно гибко управлять повтором.
retry: (failureCount, error) => {
if (error.response?.status === 404) {
return false
}
return failureCount < 2
}
retryDelay: 1000
или:
retryDelay: (attempt) => {
return attempt * 1000
}
retryDelay: (attempt) => {
return Math.min(
1000 * 2 ** attempt,
30000
)
}
Поведение:
| Попытка | Задержка |
|---|---|
| 1 | 2000 ms |
| 2 | 4000 ms |
| 3 | 8000 ms |
Optimistic update изменяет интерфейс до ответа сервера.
Если запрос завершится ошибкой — данные нужно откатить.
onMutateconst mutation = useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
await queryClient.cancelQueries({
queryKey: ['todos']
})
const previousTodos =
queryClient.getQueryData(['todos'])
queryClient.setQueryData(
['todos'],
(old) => {
return old.map((todo) =>
todo.id === newTodo.id
? newTodo
: todo
)
}
)
return { previousTodos }
},
onError: (
error,
variables,
context
) => {
queryClient.setQueryData(
['todos'],
context.previousTodos
)
}
})
Без rollback интерфейс может показать:
Удаление — одна из самых рискованных мутаций.
const deleteMutation = useMutation({
mutationFn: deletePost,
onError: () => {
toast.error(
'Не удалось удалить запись'
)
}
})
Иногда API не выбрасывает ошибку автоматически.
const mutation = useMutation({
mutationFn: async (data) => {
const response = await fetch('/api')
const result = await response.json()
if (!response.ok) {
throw new Error(result.message)
}
return result
}
})
fetch не выбрасывает исключения при 404 и
500.
Это одна из самых распространённых ошибок начинающих разработчиков.
Неправильно:
const response = await fetch('/api')
return response.json()
Правильно:
if (!response.ok) {
throw new Error('Ошибка запроса')
}
TanStack Query поддерживает интеграцию с Error Boundary.
const mutation = useMutation({
mutationFn: saveUser,
throwOnError: true
})
Теперь ошибка будет выброшена в React Error Boundary.
throwOnErrorВозможны разные варианты:
throwOnError: true
или:
throwOnError: (error) => {
return error.response?.status >= 500
}
Пример:
400 обрабатываются локально;500 отправляются в Error Boundary.Для повторной попытки может понадобиться сброс состояния.
mutation.reset()
После вызова:
isError станет false;error очистится;status вернётся к idle.<button
onCl ick={() => mutation.reset()}
>
Скрыть ошибку
</button>
Можно создать универсальную функцию.
function handleApiError(error) {
if (error.response?.status === 401) {
logout()
return
}
if (error.response?.status === 403) {
toast.error('Нет доступа')
return
}
toast.error(
error.response?.data?.message ||
'Ошибка сервера'
)
}
Использование:
const mutation = useMutation({
mutationFn: updateProfile,
onError: handleApiError
})
Часто используется интеграция:
Пример:
onError: (error) => {
Sentry.captureException(error)
}
При 401 обычно:
Пример:
onError: (error) => {
if (error.response?.status === 401) {
localStorage.removeItem('token')
queryClient.clear()
navigate('/login')
}
}
При нескольких одновременных мутациях возможны:
const isUpdating =
useIsMutating({
mutationKey: ['update-post']
}) > 0
Мутация имеет несколько состояний.
| Статус | Описание |
|---|---|
| idle | Мутация не запускалась |
| pending | Выполняется |
| success | Завершилась успешно |
| error | Завершилась ошибкой |
import { useMutation } from '@tanstack/react-query'
import axios from 'axios'
function UpdateProfile() {
const mutation = useMutation({
mutationFn: async (data) => {
const response = await axios.put(
'/api/profile',
data
)
return response.data
},
retry: (count, error) => {
if (
error.response?.status === 400
) {
return false
}
return count < 2
},
onError: (error) => {
if (error.response) {
console.log(
'Ошибка сервера'
)
} else {
console.log(
'Ошибка сети'
)
}
}
})
const handleSave = async () => {
try {
await mutation.mutateAsync({
name: 'Alex'
})
console.log('Сохранено')
} catch (error) {
console.log(error.message)
}
}
return (
<div>
<button
onCl ick={handleSave}
disabled={mutation.isPending}
>
Сохранить
</button>
{mutation.isPending && (
<p>Сохранение...</p>
)}
{mutation.isError && (
<p>
{mutation.error.message}
</p>
)}
</div>
)
}