В RTK Query mutation endpoints предназначены для изменения данных на сервере. В отличие от query endpoints, которые используются для получения информации, mutation endpoints выполняют операции создания, обновления, удаления и любые другие действия, изменяющие состояние backend-приложения.
Типичные примеры:
Mutation endpoints описываются внутри секции endpoints
при помощи метода builder.mutation.
Простейшая мутация выглядит следующим образом:
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: 'https://api.example.com'
}),
endpoints: (builder) => ({
createPost: builder.mutation({
query: (newPost) => ({
url: '/posts',
method: 'POST',
body: newPost
})
})
})
})
export const {
useCreatePostMutation
} = api
builder.query()
Используется для:
builder.mutation()
Используется для:
RTK Query автоматически генерирует hook:
useCreatePostMutation()
В отличие от query hooks, mutation hook возвращает массив:
const [createPost, result] = useCreatePostMutation()
Где:
| Элемент | Назначение |
|---|---|
createPost |
функция запуска мутации |
result |
объект состояния мутации |
const [createPost] = useCreatePostMutation()
const handleCreate = async () => {
await createPost({
title: 'New post',
content: 'Text'
})
}
Mutation endpoint обычно возвращает объект конфигурации запроса:
query: (data) => ({
url: '/posts',
method: 'POST',
body: data
})
addUser: builder.mutation({
query: (user) => ({
url: '/users',
method: 'POST',
body: user
})
})
updateUser: builder.mutation({
query: ({ id, ...user }) => ({
url: `/users/${id}`,
method: 'PUT',
body: user
})
})
patchUser: builder.mutation({
query: ({ id, ...patch }) => ({
url: `/users/${id}`,
method: 'PATCH',
body: patch
})
})
deleteUser: builder.mutation({
query: (id) => ({
url: `/users/${id}`,
method: 'DELETE'
})
})
Mutation hook предоставляет объект состояния:
const [
createPost,
{
data,
error,
isLoading,
isSuccess,
isError
}
] = useCreatePostMutation()
Mutation выполняется в данный момент.
if (isLoading) {
return <p>Saving...</p>
}
Mutation успешно завершена.
if (isSuccess) {
console.log('Saved')
}
Во время запроса произошла ошибка.
if (isError) {
console.error(error)
}
После успешного выполнения серверный ответ попадает в
data.
const [login, { data }] = useLoginMutation()
const handleLogin = async () => {
await login({
email: 'admin@mail.com',
password: '123'
})
}
После выполнения:
console.log(data)
Метод unwrap() преобразует mutation promise в обычный
promise.
Без unwrap() RTK Query не выбрасывает ошибку через
catch.
try {
const result = await createPost(post)
console.log(result)
} catch (error) {
console.log(error)
}
try {
const response = await createPost(post).unwrap()
console.log(response)
} catch (error) {
console.error(error)
}
unwrap() особенно полезен:
Mutation принимает один аргумент.
updatePost: builder.mutation({
query: ({ id, title, content }) => ({
url: `/posts/${id}`,
method: 'PATCH',
body: {
title,
content
}
})
})
updatePost({
id: 15,
title: 'Updated',
content: 'New content'
})
Функция query получает аргумент mutation:
query: (credentials) => ({
url: '/login',
method: 'POST',
body: credentials
})
deleteComment: builder.mutation({
query: (commentId) => ({
url: `/comments/${commentId}`,
method: 'DELETE'
})
})
publishPost: builder.mutation({
query: ({ id, notify }) => ({
url: `/posts/${id}`,
method: 'POST',
params: {
notify
}
})
})
uploadAvatar: builder.mutation({
query: (formData) => ({
url: '/avatar',
method: 'POST',
body: formData,
headers: {
Authorization: 'Bearer token'
}
})
})
RTK Query поддерживает отправку файлов.
uploadFile: builder.mutation({
query: (file) => {
const formData = new FormData()
formData.append('file', file)
return {
url: '/upload',
method: 'POST',
body: formData
}
}
})
Mutation часто используется для login/logout.
login: builder.mutation({
query: (credentials) => ({
url: '/auth/login',
method: 'POST',
body: credentials
})
})
const [login, { isLoading }] = useLoginMutation()
const handleSubmit = async () => {
try {
const response = await login({
email: 'admin@mail.com',
password: '123456'
}).unwrap()
console.log(response.token)
} catch (error) {
console.error(error)
}
}
Одной из важнейших возможностей mutation endpoints является автоматическая инвалидизация кэша.
После изменения данных RTK Query может автоматически обновить связанные query endpoints.
getPosts: builder.query({
query: () => '/posts',
providesTags: ['Posts']
})
createPost: builder.mutation({
query: (post) => ({
url: '/posts',
method: 'POST',
body: post
}),
invalidatesTags: ['Posts']
})
Последовательность:
getPosts: builder.query({
query: () => '/posts',
providesTags: (result) =>
result
? [
...result.map(({ id }) => ({
type: 'Posts',
id
})),
{ type: 'Posts', id: 'LIST' }
]
: [{ type: 'Posts', id: 'LIST' }]
})
updatePost: builder.mutation({
query: ({ id, ...patch }) => ({
url: `/posts/${id}`,
method: 'PATCH',
body: patch
}),
invalidatesTags: (result, error, arg) => [
{ type: 'Posts', id: arg.id }
]
})
createPost: builder.mutation({
query: (post) => ({
url: '/posts',
method: 'POST',
body: post
}),
invalidatesTags: [
{ type: 'Posts', id: 'LIST' }
]
})
RTK Query поддерживает optimistic updates через
onQueryStarted.
Этот lifecycle callback вызывается сразу после запуска mutation.
updatePost: builder.mutation({
query: ({ id, ...patch }) => ({
url: `/posts/${id}`,
method: 'PATCH',
body: patch
}),
async onQueryStarted(arg, { dispatch, queryFulfilled }) {
}
})
updatePost: builder.mutation({
query: ({ id, ...patch }) => ({
url: `/posts/${id}`,
method: 'PATCH',
body: patch
}),
async onQueryStarted(
{ id, ...patch },
{ dispatch, queryFulfilled }
) {
const patchResult = dispatch(
api.util.updateQueryData(
'getPosts',
undefined,
(draft) => {
const post = draft.find(
(item) => item.id === id
)
if (post) {
Object.assign(post, patch)
}
}
)
)
try {
await queryFulfilled
} catch {
patchResult.undo()
}
}
})
Метод:
api.util.updateQueryData()
позволяет изменять кэш query вручную.
api.util.updateQueryData(
endpointName,
queryArg,
updateCallback
)
Название query endpoint.
'getPosts'
Аргумент query.
undefined
Функция изменения draft-state.
(draft) => {
draft.push(newPost)
}
При ошибке можно откатить изменения:
patchResult.undo()
Иногда необходимо дождаться ответа сервера.
createPost: builder.mutation({
query: (post) => ({
url: '/posts',
method: 'POST',
body: post
}),
async onQueryStarted(arg, {
dispatch,
queryFulfilled
}) {
try {
const { data: createdPost } =
await queryFulfilled
dispatch(
api.util.updateQueryData(
'getPosts',
undefined,
(draft) => {
draft.push(createdPost)
}
)
)
} catch (error) {
console.error(error)
}
}
})
Mutation поддерживает преобразование ответа.
createPost: builder.mutation({
query: (post) => ({
url: '/posts',
method: 'POST',
body: post
}),
transformResponse: (response) => {
return response.data
}
})
Можно преобразовывать ошибки.
login: builder.mutation({
query: (credentials) => ({
url: '/login',
method: 'POST',
body: credentials
}),
transformErrorResponse: (response) => {
return response.data.message
}
})
По умолчанию каждая mutation хранит собственное состояние.
Иногда требуется разделять состояние между компонентами.
const [
login,
loginState
] = useLoginMutation({
fixedCacheKey: 'shared-login'
})
Подходит для:
Mutation state можно очищать вручную.
const [
createPost,
{ reset }
] = useCreatePostMutation()
reset()
Mutation endpoints не поддерживают polling, поскольку они не предназначены для постоянного получения данных.
Polling относится только к query endpoints.
Полный жизненный цикл mutation:
RTK Query создаёт Redux actions:
api/executeMutation/pending
api/executeMutation/fulfilled
api/executeMutation/rejected
В DevTools можно отслеживать:
Mutation hook вызывает re-render:
Можно минимизировать лишние re-render.
const [updatePost, { isLoading }] =
useUpdatePostMutation({
selectFromResult: ({
isLoading
}) => ({
isLoading
})
})
Разрешается использовать несколько hooks:
const [createPost] = useCreatePostMutation()
const [deletePost] = useDeletePostMutation()
const [updatePost] = useUpdatePostMutation()
await createPost(post).unwrap()
await publishPost(postId).unwrap()
await Promise.all([
updatePost(post1),
updatePost(post2),
updatePost(post3)
])
Mutation promise поддерживает abort.
const promise = createPost(post)
promise.abort()
RTK Query может использовать retry wrapper.
import {
retry,
fetchBaseQuery
} from '@reduxjs/toolkit/query/react'
const baseQuery = retry(
fetchBaseQuery({
baseUrl: '/api'
}),
{
maxRetries: 3
}
)
Mutation работает с любым baseQuery.
const axiosBaseQuery =
({ baseUrl }) =>
async ({
url,
method,
data
}) => {
try {
const result = await axios({
url: baseUrl + url,
method,
data
})
return {
data: result.data
}
} catch (axiosError) {
return {
error: {
status:
axiosError.response?.status,
data:
axiosError.response?.data
}
}
}
}
createPost: builder.mutation({
query: (post) => ({
url: '/posts',
method: 'POST',
data: post
})
})
Неправильно:
builder.query({
query: (post) => ({
url: '/posts',
method: 'POST',
body: post
})
})
Правильно:
builder.mutation({
query: (post) => ({
url: '/posts',
method: 'POST',
body: post
})
})
Без invalidation UI может показывать устаревшие данные.
Без unwrap() сложнее обрабатывать ошибки через
try/catch.
Неправильно:
draft = newData
Правильно:
Object.assign(draft, newData)
Хорошая практика:
postsApi
usersApi
commentsApi
authApi
createPost
updatePost
deletePost
publishPost
archivePost
tagTypes: [
'Posts',
'Users',
'Comments'
]
Сложную optimistic-логику желательно выносить:
Mutation обычно не используется во время SSR, поскольку изменение данных должно происходить на клиенте после hydration.
Mutation endpoints можно комбинировать с realtime-обновлениями:
updateQueryData.RTK Query особенно хорошо подходит для форм.
const [createUser, {
isLoading,
error
}] = useCreateUserMutation()
const handleSubmit = async (values) => {
try {
await createUser(values).unwrap()
} catch (error) {
console.error(error)
}
}
const onSub mit = async (data) => {
await updateProfile(data).unwrap()
}
RTK Query автоматически синхронизирует:
Иногда invalidation недостаточно.
В таких случаях используются:
updateQueryData
upsertQueryData
invalidateTags
Позволяет создавать или заменять кэш.
dispatch(
api.util.upsertQueryData(
'getPost',
post.id,
post
)
)
dispatch(
api.util.invalidateTags([
'Posts'
])
)
Mutation endpoint способен выполнять:
Это делает RTK Query полноценной системой управления серверным состоянием, а не просто инструментом для HTTP-запросов.