В основе работы RTK Query лежит система кэширования запросов, где каждый результат ассоциирован с набором тегов (tags). Эти теги формируют декларативную модель зависимости данных, позволяя автоматически управлять обновлением кэша при изменении состояния на сервере.
Ключевая идея заключается в том, что запросы не инвалидируются вручную в классическом императивном стиле. Вместо этого описывается связь: какие данные предоставляет endpoint и какие данные он затрагивает при мутациях.
RTK Query использует механизм providesTags и
invalidatesTags.
providesTags — описывает, какие сущности формирует
запросinvalidatesTags — описывает, какие сущности изменяет
мутацияgetPosts: builder.query({
query: () => '/posts',
providesTags: ['Posts']
})
addPost: builder.mutation({
query: (post) => ({
url: '/posts',
method: 'POST',
body: post
}),
invalidatesTags: ['Posts']
})
В этом случае любое добавление поста приводит к автоматическому рефетчу списка постов.
Простые строки в тегах подходят только для глобальной инвалидации.
Более точный контроль достигается через объектные теги с
type и id.
getPosts: builder.query({
query: () => '/posts',
providesTags: (result) =>
result
? [
...result.map(({ id }) => ({ type: 'Post', id })),
{ type: 'Post', id: 'LIST' }
]
: [{ type: 'Post', id: 'LIST' }]
})
Такой подход формирует два уровня кэш-сущностей:
Post/1, Post/2)Post/LIST)При использовании структурированных тегов появляется возможность инвалидировать только изменённую сущность.
updatePost: builder.mutation({
query: ({ id, ...patch }) => ({
url: `/posts/${id}`,
method: 'PATCH',
body: patch
}),
invalidatesTags: (result, error, { id }) => [
{ type: 'Post', id }
]
})
Результат — обновляется только конкретный пост, без повторного запроса всего списка.
Список часто рассматривается как отдельная сущность. При добавлении или удалении элементов он должен обновляться целиком.
deletePost: builder.mutation({
query: (id) => ({
url: `/posts/${id}`,
method: 'DELETE'
}),
invalidatesTags: [
{ type: 'Post', id: 'LIST' }
]
})
Использование LIST позволяет отделить операции над
коллекцией от операций над отдельными сущностями.
В реальных приложениях часто требуется комбинировать разные уровни инвалидирования.
updatePost: builder.mutation({
query: ({ id, ...patch }) => ({
url: `/posts/${id}`,
method: 'PATCH',
body: patch
}),
invalidatesTags: (result, error, { id }) => [
{ type: 'Post', id },
{ type: 'Post', id: 'LIST' }
]
})
Такой подход применяется, когда:
RTK Query позволяет динамически формировать теги в зависимости от ответа сервера.
updateUser: builder.mutation({
query: (user) => ({
url: `/users/${user.id}`,
method: 'PUT',
body: user
}),
invalidatesTags: (result) => {
if (!result) return []
return [
{ type: 'User', id: result.id },
result.isAdmin && { type: 'AdminStats', id: 'SUMMARY' }
].filter(Boolean)
}
})
Такой подход позволяет учитывать бизнес-логику при выборе того, какие части кэша должны быть обновлены.
Один запрос может затрагивать несколько независимых доменов данных.
createComment: builder.mutation({
query: (comment) => ({
url: '/comments',
method: 'POST',
body: comment
}),
invalidatesTags: [
{ type: 'Comment', id: 'LIST' },
{ type: 'Post', id: comment.postId }
]
})
Здесь происходит синхронное обновление:
Связанные сущности часто требуют каскадного обновления.
getPost: builder.query({
query: (id) => `/posts/${id}`,
providesTags: (result, error, id) => [
{ type: 'Post', id },
{ type: 'Post', id: 'DETAIL' }
]
})
updatePostTitle: builder.mutation({
query: ({ id, title }) => ({
url: `/posts/${id}`,
method: 'PATCH',
body: { title }
}),
invalidatesTags: [
{ type: 'Post', id: 'DETAIL' },
{ type: 'Post', id }
]
})
Такая модель используется, когда один и тот же ресурс может отображаться в разных представлениях (список, детальная карточка, превью).
В некоторых случаях теги не используются, и обновление происходит
через refetch вручную или через
refetchOnMountOrArgChange.
getPosts: builder.query({
query: () => '/posts',
refetchOnMountOrArgChange: true
})
Этот подход снижает гибкость, но может быть оправдан при простых сценариях или при отсутствии необходимости в точной синхронизации кэша.
RTK Query предоставляет API для ручного управления кэшем через
dispatch.
dispatch(api.util.invalidateTags([{ type: 'Post', id: 'LIST' }]))
Использование такого механизма характерно для:
При интеграции с WebSocket или polling модель инвалидации становится событийной.
socket.on('postUpdated', (post) => {
dispatch(
api.util.invalidateTags([{ type: 'Post', id: post.id }])
)
})
Такая схема позволяет синхронизировать кэш с сервером без повторных мутаций через RTK Query.
Чрезмерное использование глобальных тегов приводит к лишним рефетчам. Для оптимизации применяются:
id-тегов вместо
глобальных типовПлохой вариант:
invalidatesTags: ['Post']
Хороший вариант:
invalidatesTags: [{ type: 'Post', id: 'LIST' }]
Модель RTK Query можно рассматривать как ориентированный граф:
Инвалидация в этом контексте — это операция обхода графа и удаления связанных узлов. Такой подход позволяет масштабировать логику обновления данных без ручного управления состоянием.
Частые проблемы при работе с инвалидацией:
Post vs
Posts)Эти ошибки приводят к избыточным сетевым запросам и потере преимуществ кэширования.
Критически важно поддерживать единый реестр типов тегов в рамках API slice.
tagTypes: ['Post', 'User', 'Comment']
Несовпадение имен приводит к тому, что инвалидация перестаёт работать, несмотря на корректную структуру кода.