Условная инвалидация в RTK Query — механизм выборочного сброса кэша, при котором обновление данных выполняется только при соблюдении определённых условий. В отличие от безусловной инвалидации, где запросы автоматически помечаются устаревшими после каждой mutation-операции, условный подход позволяет контролировать процесс обновления значительно точнее.
Такой механизм особенно важен в крупных приложениях, где:
RTK Query предоставляет несколько способов реализации условной инвалидации:
invalidatesTags;providesTags;api.util.invalidateTags;Обычная инвалидация выглядит так:
updatePost: builder.mutation({
query: ({ id, ...body }) => ({
url: `posts/${id}`,
method: 'PATCH',
body
}),
invalidatesTags: ['Post']
})
После выполнения mutation RTK Query инвалидирует все запросы с тегом
Post.
Условная инвалидация позволяет определить логику динамически:
updatePost: builder.mutation({
query: ({ id, ...body }) => ({
url: `posts/${id}`,
method: 'PATCH',
body
}),
invalidatesTags: (result, error, arg) => {
if (error) {
return []
}
return ['Post']
}
})
Если mutation завершилась ошибкой — инвалидация не выполняется.
Функция получает три параметра:
invalidatesTags: (result, error, arg) => {
return []
}
Содержит успешный ответ сервера.
invalidatesTags: (result) => {
console.log(result)
return ['Post']
}
Пример ответа:
{
id: 15,
title: 'New title',
published: true
}
Содержит объект ошибки.
invalidatesTags: (result, error) => {
if (error) {
console.error(error)
}
return []
}
Аргументы mutation-запроса.
updatePost: builder.mutation({
query: ({ id, status }) => ({
url: `posts/${id}`,
method: 'PATCH',
body: { status }
}),
invalidatesTags: (result, error, arg) => {
console.log(arg)
return ['Post']
}
})
Наиболее распространённый сценарий.
invalidatesTags: (result, error) => {
if (error) {
return []
}
return ['Post']
}
Проблема безусловной инвалидации заключается в том, что даже при ошибке сервера RTK Query всё равно обновляет связанные query-запросы.
Это приводит к:
Условная проверка устраняет проблему.
Сервер может вернуть информацию о фактических изменениях.
Пример:
{
success: true,
updated: false
}
Инвалидация выполняется только при реальном обновлении:
invalidatesTags: (result) => {
if (!result?.updated) {
return []
}
return ['Post']
}
Часто требуется обновлять кэш только при определённом статусе сущности.
updatePost: builder.mutation({
query: ({ id, ...body }) => ({
url: `posts/${id}`,
method: 'PATCH',
body
}),
invalidatesTags: (result) => {
if (!result?.published) {
return []
}
return ['Post']
}
})
Черновики не вызывают обновление списка опубликованных материалов.
Условная логика может зависеть от отправленных параметров.
invalidatesTags: (result, error, arg) => {
if (arg.skipCache) {
return []
}
return ['Post']
}
Вызов:
updatePost({
id: 1,
title: 'New title',
skipCache: true
})
Одна из важнейших возможностей RTK Query — выборочное обновление отдельных записей.
invalidatesTags: ['Post']
Обновляются:
invalidatesTags: (result, error, arg) => {
if (error) {
return []
}
return [
{
type: 'Post',
id: arg.id
}
]
}
Теперь обновляется только конкретный пост.
Часто изменение записи влияет и на список.
invalidatesTags: (result, error, arg) => {
if (error) {
return []
}
return [
{ type: 'Post', id: arg.id },
{ type: 'Post', id: 'LIST' }
]
}
RTK Query не имеет встроенного специального списка. Обычно
используется условный идентификатор LIST.
getPosts: builder.query({
query: () => 'posts',
providesTags: (result) =>
result
? [
...result.map(post => ({
type: 'Post',
id: post.id
})),
{ type: 'Post', id: 'LIST' }
]
: [{ type: 'Post', id: 'LIST' }]
})
createPost: builder.mutation({
query: (body) => ({
url: 'posts',
method: 'POST',
body
}),
invalidatesTags: (result, error) => {
if (error) {
return []
}
return [
{ type: 'Post', id: 'LIST' }
]
}
})
Создание нового поста обновляет только список, а не все существующие элементы.
Mutation может выполнять разные действия.
updatePost: builder.mutation({
query: ({ id, action }) => ({
url: `posts/${id}`,
method: 'PATCH',
body: { action }
}),
invalidatesTags: (result, error, arg) => {
if (arg.action === 'view') {
return []
}
if (arg.action === 'like') {
return [
{ type: 'Post', id: arg.id }
]
}
return ['Post']
}
})
Удаление требует отдельного подхода.
deletePost: builder.mutation({
query: (id) => ({
url: `posts/${id}`,
method: 'DELETE'
}),
invalidatesTags: (result, error, id) => {
if (error) {
return []
}
return [
{ type: 'Post', id },
{ type: 'Post', id: 'LIST' }
]
}
})
RTK Query позволяет инвалидировать кэш вручную.
dispatch(
api.util.invalidateTags([
'Post'
])
)
Главное преимущество — возможность выполнить проверку перед обновлением.
const handleSave = async () => {
const result = await updatePost(data)
if ('data' in result) {
dispatch(
api.util.invalidateTags([
'Post'
])
)
}
}
Lifecycle-обработчик предоставляет полный контроль.
updatePost: builder.mutation({
query: ({ id, ...body }) => ({
url: `posts/${id}`,
method: 'PATCH',
body
}),
async onQueryStarted(arg, api) {
try {
await api.queryFulfilled
api.dispatch(
apiSlice.util.invalidateTags([
{
type: 'Post',
id: arg.id
}
])
)
} catch (error) {
}
}
})
Во многих случаях повторный запрос вообще не нужен.
async onQueryStarted(arg, api) {
const patchResult = api.dispatch(
apiSlice.util.updateQueryData(
'getPost',
arg.id,
(draft) => {
draft.title = arg.title
}
)
)
try {
await api.queryFulfilled
} catch {
patchResult.undo()
}
}
invalidatesTags: (result, error) => {
if (error) {
return ['Post']
}
return []
}
Такой подход:
Пример сложной бизнес-логики.
invalidatesTags: (result, error, arg) => {
if (arg.userRole !== 'admin') {
return []
}
return ['Users']
}
Некоторые API возвращают состояние синхронизации.
{
synced: true
}
invalidatesTags: (result) => {
if (!result?.synced) {
return []
}
return ['Settings']
}
invalidatesTags: (result, error, arg) => {
const tags = []
if (arg.updatePost) {
tags.push('Post')
}
if (arg.updateUser) {
tags.push('User')
}
if (arg.updateComments) {
tags.push('Comment')
}
return tags
}
RTK Query позволяет анализировать структуру ошибки.
invalidatesTags: (result, error) => {
if (error?.status === 409) {
return ['Post']
}
return []
}
Массовые обновления требуют отдельного подхода.
bulkUpdatePosts: builder.mutation({
query: (ids) => ({
url: 'posts/bulk',
method: 'PATCH',
body: ids
}),
invalidatesTags: (result, error, ids) => {
if (error) {
return []
}
return ids.map(id => ({
type: 'Post',
id
}))
}
})
Изменение одной сущности может затрагивать другие.
updateComment: builder.mutation({
query: ({ id, ...body }) => ({
url: `comments/${id}`,
method: 'PATCH',
body
}),
invalidatesTags: (result) => {
if (!result) {
return []
}
return [
{
type: 'Comment',
id: result.id
},
{
type: 'Post',
id: result.postId
}
]
}
})
invalidatesTags: (result) => {
if (!result?.affectedIds) {
return []
}
return result.affectedIds.map(id => ({
type: 'Post',
id
}))
}
Неправильно:
invalidatesTags: (result) => {
if (!result) {
return
}
return ['Post']
}
Правильно:
invalidatesTags: (result) => {
if (!result) {
return []
}
return ['Post']
}
invalidatesTags: [
{ type: 'Post', id: 'LIST' }
]
Даже минимальное изменение вызывает полную перезагрузку списка.
Плохо:
return ['Post', 'User', 'Comment', 'Profile']
Это приводит к каскадным запросам.
Всегда предпочтительно инвалидировать:
{ type: 'Post', id }
вместо:
['Post']
Оптимальный подход:
{ type: 'Post', id: 'LIST' }
и
{ type: 'Post', id: 15 }
должны использоваться отдельно.
tagTypes: [
'Post',
'User',
'Comment'
]
const LIST_ID = 'LIST'
const invalidatePost = (id) => [
{ type: 'Post', id }
]
updatePost: builder.mutation({
query: ({ id, ...body }) => ({
url: `posts/${id}`,
method: 'PATCH',
body
}),
async onQueryStarted(arg, api) {
const patchResult = api.dispatch(
apiSlice.util.updateQueryData(
'getPost',
arg.id,
(draft) => {
Object.assign(draft, arg)
}
)
)
try {
await api.queryFulfilled
} catch {
patchResult.undo()
api.dispatch(
apiSlice.util.invalidateTags([
{
type: 'Post',
id: arg.id
}
])
)
}
},
invalidatesTags: (result, error, arg) => {
if (error) {
return []
}
if (result?.requiresFullReload) {
return [
{ type: 'Post', id: 'LIST' }
]
}
return [
{
type: 'Post',
id: arg.id
}
]
}
})
Такая схема сочетает: