Система тегов в RTK Query представляет собой механизм логической
связи между запросами (query) и мутациями
(mutation). Теги используются для автоматической
инвалидации кэша и повторного обновления данных после изменений на
сервере.
При неправильной организации тегов приложение начинает выполнять избыточные сетевые запросы, создавать каскадные перезагрузки данных и перегружать клиентский кэш. В крупных приложениях это приводит к ухудшению производительности, росту количества ререндеров и повышенной нагрузке на API.
Оптимизация тегов направлена на решение нескольких задач:
Одна из самых распространённых ошибок — использование одного общего тега для всех запросов.
Пример неоптимальной конфигурации:
tagTypes: ['Posts']
getPosts: build.query({
query: () => '/posts',
providesTags: ['Posts']
})
getPost: build.query({
query: (id) => `/posts/${id}`,
providesTags: ['Posts']
})
updatePost: build.mutation({
query: ({ id, ...body }) => ({
url: `/posts/${id}`,
method: 'PUT',
body
}),
invalidatesTags: ['Posts']
})
После обновления одной записи будут инвалидированы:
Если приложение содержит десятки открытых компонентов, количество повторных запросов может резко увеличиться.
Наиболее эффективный подход — разделение тегов по идентификаторам сущностей.
getPost: build.query({
query: (id) => `/posts/${id}`,
providesTags: (result, error, id) => [
{ type: 'Posts', id }
]
})
updatePost: build.mutation({
query: ({ id, ...body }) => ({
url: `/posts/${id}`,
method: 'PUT',
body
}),
invalidatesTags: (result, error, { id }) => [
{ type: 'Posts', id }
]
})
Теперь RTK Query обновит только изменённый объект.
Преимущества:
Наиболее распространённая архитектура в RTK Query — использование специальных тегов списка.
Если инвалидировать только конкретный элемент:
invalidatesTags: [{ type: 'Posts', id: 5 }]
то список постов (getPosts) не обновится.
Это приводит к рассинхронизации:
RTK Query рекомендует разделять:
getPosts: build.query({
query: () => '/posts',
providesTags: (result) =>
result
? [
...result.map(post => ({
type: 'Posts',
id: post.id
})),
{ type: 'Posts', id: 'LIST' }
]
: [{ type: 'Posts', id: 'LIST' }]
})
Мутация создания:
createPost: build.mutation({
query: (body) => ({
url: '/posts',
method: 'POST',
body
}),
invalidatesTags: [
{ type: 'Posts', id: 'LIST' }
]
})
Мутация обновления:
updatePost: build.mutation({
query: ({ id, ...body }) => ({
url: `/posts/${id}`,
method: 'PUT',
body
}),
invalidatesTags: (result, error, { id }) => [
{ type: 'Posts', id }
]
})
Мутация удаления:
deletePost: build.mutation({
query: (id) => ({
url: `/posts/${id}`,
method: 'DELETE'
}),
invalidatesTags: (result, error, id) => [
{ type: 'Posts', id },
{ type: 'Posts', id: 'LIST' }
]
})
Очень частая ошибка:
invalidatesTags: [
{ type: 'Posts', id: 'LIST' }
]
для любой мутации.
Даже обновление одного поля поста вызовет:
Если список содержит сотни элементов, это становится дорогостоящей операцией.
Крупные приложения не должны использовать один tagType для всех сущностей.
Плохой пример:
tagTypes: ['Data']
Правильный пример:
tagTypes: [
'Posts',
'Users',
'Comments',
'Categories',
'Notifications'
]
Преимущества:
Иногда разработчики создают теги для каждого вложенного объекта:
providesTags: (result) =>
result.flatMap(post => [
{ type: 'Posts', id: post.id },
...post.comments.map(comment => ({
type: 'Comments',
id: comment.id
}))
])
Если список содержит тысячи комментариев:
Слишком granular tags также ухудшают производительность.
Например:
{ type: 'Posts', id: 'title-5' }
{ type: 'Posts', id: 'content-5' }
{ type: 'Posts', id: 'author-5' }
Такой уровень детализации редко оправдан.
RTK Query оптимален при работе:
Для пагинации эффективнее использовать теги страниц.
getPosts: build.query({
query: (page) => `/posts?page=${page}`,
providesTags: (result, error, page) => [
{ type: 'Posts', id: `PAGE-${page}` }
]
})
Инвалидация:
invalidatesTags: [
{ type: 'Posts', id: 'PAGE-2' }
]
Преимущества:
В сложных системах используется комбинация:
providesTags: (result, error, page) =>
result
? [
...result.items.map(post => ({
type: 'Posts',
id: post.id
})),
{ type: 'Posts', id: `PAGE-${page}` },
{ type: 'Posts', id: 'LIST' }
]
: [
{ type: 'Posts', id: `PAGE-${page}` },
{ type: 'Posts', id: 'LIST' }
]
Такая архитектура позволяет:
При бесконечной прокрутке неправильная инвалидация особенно опасна.
Если инвалидировать:
{ type: 'Posts', id: 'LIST' }
то RTK Query может повторно загрузить:
Для infinite scroll лучше разделять данные:
{ type: 'Feed', id: 'CHUNK-1' }
{ type: 'Feed', id: 'CHUNK-2' }
{ type: 'Feed', id: 'CHUNK-3' }
Это позволяет обновлять только изменённые сегменты.
Не все запросы нуждаются в тегах.
Пример:
getCountries: build.query({
query: () => '/countries'
})
Если список стран:
теги становятся лишними.
Избыточное использование тегов:
В сложных API часто встречаются вложенные структуры.
/users/5/posts
/users/5/comments
/users/5/friends
Ошибка:
tagTypes: ['Users']
Правильнее:
tagTypes: [
'Users',
'UserPosts',
'UserComments',
'UserFriends'
]
Это уменьшает область инвалидации.
Если используется createEntityAdapter, система тегов
должна учитывать нормализованную структуру.
providesTags: (result) =>
result
? [
...result.ids.map(id => ({
type: 'Posts',
id
})),
{ type: 'Posts', id: 'LIST' }
]
: [{ type: 'Posts', id: 'LIST' }]
Такой подход хорошо масштабируется на большие объёмы данных.
Частая ошибка — сочетание агрессивной инвалидации с автоматическим рефетчем:
refetchOnFocus: true
refetchOnReconnect: true
Если теги настроены неоптимально:
При использовании optimistic updates часть инвалидаций можно исключить полностью.
async onQueryStarted(arg, { dispatch, queryFulfilled }) {
const patch = dispatch(
api.util.updateQueryData(
'getPost',
arg.id,
draft => {
draft.title = arg.title
}
)
)
try {
await queryFulfilled
} catch {
patch.undo()
}
}
Если optimistic upd ate полностью синхронизирует данные:
invalidatesTags: []
может быть достаточным решением.
Это особенно полезно:
Иногда providesTags случайно создаёт дубликаты.
Плохой пример:
providesTags: (result) =>
result.map(item => ({
type: 'Posts',
id: item.id
}))
если API возвращает повторяющиеся элементы.
Оптимизация:
providesTags: (result) => {
const uniqueIds = [...new Se t(
result.map(item => item.id)
)]
return uniqueIds.map(id => ({
type: 'Posts',
id
}))
}
Каждый тег хранится внутри RTK Query state.
Тысячи тегов могут приводить к:
Иногда эффективнее использовать логические группы.
{ type: 'Posts', id: 'TRENDING' }
{ type: 'Posts', id: 'LATEST' }
{ type: 'Posts', id: 'ARCHIVE' }
Это полезно:
RTK Query позволяет комбинировать теги.
invalidatesTags: (result, error, post) => [
{ type: 'Posts', id: post.id },
{ type: 'Users', id: post.userId },
{ type: 'Statistics', id: 'POSTS_COUNT' }
]
Подобная архитектура должна использоваться осторожно.
Слишком большое количество зависимостей:
При масштабировании приложения полезно анализировать:
Основные признаки проблем:
Подход:
LIST + ENTITY
обычно полностью достаточен.
Рекомендуется:
Эффективны:
Для большинства production-приложений оптимальной считается следующая архитектура:
tagTypes: [
'Posts',
'Users',
'Comments'
]
Схема тегов:
LIST
PAGE-x
ENTITY_ID
FILTER-x
CATEGORY-x
Принципы: